@softr/cli 0.1.6 → 0.1.7-se22117.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +6 -2
  2. package/dist/cli.mjs +131 -46
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -188,9 +188,13 @@ synced. That is how `push` knows there is nothing to send, how it notices that t
188
188
 
189
189
  #### The node types on offer
190
190
 
191
- `spec` lists them. The set is small on purpose: the generic triggers (webhook, schedules, Softr Tables and Softr Apps
191
+ `spec` lists the curated set, small on purpose: the generic triggers (webhook, schedules, Softr Tables and Softr Apps
192
192
  events, incoming email), the control nodes (branch, filter, loop, wait), `RESPONDED_TO_WEBHOOK`, `CALL_API`, and
193
- `CUSTOM_CODE` for everything else. Other integrations stay in the studio. Each entry points to its public docs page.
193
+ `CUSTOM_CODE` for code. Each entry points to its public docs page and comes with a template and an example.
194
+
195
+ Any other node type the workflows service knows works in the file as well; the CLI just has no specification for it.
196
+ `spec <TYPE>` says where to get one (the Softr MCP server or the studio), `add <TYPE> --node-version <v>` writes the node
197
+ with empty inputs, and the service validates it on publish, as for every node.
194
198
 
195
199
  #### A typical session
196
200
 
package/dist/cli.mjs CHANGED
@@ -6070,18 +6070,74 @@ var CATALOG = catalog_default;
6070
6070
  function catalogEntries(kind) {
6071
6071
  return CATALOG.filter((entry) => !kind || entry.kind === kind);
6072
6072
  }
6073
- function findCatalogEntry(type, kind) {
6073
+ function catalogEntryOrNull(type, kind) {
6074
6074
  const wanted = type.trim().toUpperCase();
6075
- const entry = CATALOG.find((candidate) => candidate.type === wanted && (!kind || candidate.kind === kind));
6075
+ return CATALOG.find((candidate) => candidate.type === wanted && (!kind || candidate.kind === kind));
6076
+ }
6077
+ function findCatalogEntry(type, kind) {
6078
+ const entry = catalogEntryOrNull(type, kind);
6076
6079
  if (!entry) {
6077
6080
  const known = catalogEntries(kind).map((candidate) => candidate.type).join(", ");
6078
6081
  throw new CliError(
6079
- `"${type}" is not a node type this CLI offers${kind ? ` as a ${kind}` : ""}.`,
6080
- `Available: ${known}. The CLI is limited to code-first workflows on purpose; other integrations are configured in the studio.`
6082
+ `"${type.trim().toUpperCase()}" is not in the CLI's curated set${kind ? ` of ${kind}s` : ""}.`,
6083
+ `Curated: ${known}.
6084
+ ${uncuratedGuidance(type)}`
6081
6085
  );
6082
6086
  }
6083
6087
  return entry;
6084
6088
  }
6089
+ function uncuratedGuidance(type) {
6090
+ const wanted = type.trim().toUpperCase();
6091
+ return [
6092
+ `${wanted} is not in the curated set, so the CLI has no template or specification for it; the workflows service`,
6093
+ "still runs it. Take its specification (inputs, the required ones, fixed options, version) from the Softr MCP server",
6094
+ "(get_node_specifications; workflow_get_node_specifications in an agent runtime) or from the studio. Then write the",
6095
+ "node with its type, version and inputs, or scaffold it: softr workflows add <TYPE> --node-version <v> [--kind trigger].",
6096
+ "Push and test it as any node: the service validates it on publish and names what is wrong."
6097
+ ].join("\n");
6098
+ }
6099
+ function resolveNodeType(type, choice) {
6100
+ const entry = catalogEntryOrNull(type, choice.kind);
6101
+ if (entry) {
6102
+ if (choice.version) {
6103
+ throw new CliError(
6104
+ `${entry.type} is curated: the CLI pins its version (${entry.version}).`,
6105
+ 'Drop --node-version; "softr workflows upgrade" moves nodes to the current one.'
6106
+ );
6107
+ }
6108
+ const { type: curatedType, kind, label, version: version2, template } = entry;
6109
+ return { type: curatedType, kind, label, version: version2, template: structuredClone(template), curated: true };
6110
+ }
6111
+ const otherKind = catalogEntryOrNull(type);
6112
+ if (otherKind) {
6113
+ throw new CliError(
6114
+ `${otherKind.type} is curated as ${article(otherKind.kind)}, not ${article(choice.kind ?? "action")}.`,
6115
+ `Curated ${choice.kind ?? "action"}s: ${catalogEntries(choice.kind ?? "action").map((candidate) => candidate.type).join(", ")}.`
6116
+ );
6117
+ }
6118
+ const wanted = type.trim().toUpperCase();
6119
+ if (!choice.version) {
6120
+ throw new CliError(
6121
+ `"${wanted}" is not in the CLI's curated set, so pass --node-version <v>: the node version the service runs.`,
6122
+ uncuratedGuidance(type)
6123
+ );
6124
+ }
6125
+ return {
6126
+ type: wanted,
6127
+ kind: choice.kind ?? "action",
6128
+ label: labelOf(wanted),
6129
+ version: choice.version,
6130
+ template: {},
6131
+ curated: false
6132
+ };
6133
+ }
6134
+ function article(kind) {
6135
+ return kind === "action" ? "an action" : "a trigger";
6136
+ }
6137
+ function labelOf(type) {
6138
+ const words = type.toLowerCase().split("_");
6139
+ return [words[0].charAt(0).toUpperCase() + words[0].slice(1), ...words.slice(1)].join(" ");
6140
+ }
6085
6141
  function isTestableType(type) {
6086
6142
  return !NOT_TESTABLE_TYPES.has(type);
6087
6143
  }
@@ -14346,42 +14402,48 @@ function pathsAt(config, pathsPath) {
14346
14402
  return Array.isArray(current2) ? current2 : [];
14347
14403
  }
14348
14404
  function registerEdit(program3) {
14349
- program3.command("replace").description("Give a node another type: keeps its id, title and paths; inputs start from the new type's template").argument("<nodeId>", "the trigger or action to change").argument("<type>", 'the new type from "spec", of the same kind (trigger or action)').option("--lang <language>", "for CUSTOM_CODE: JAVASCRIPT or PYTHON (default JAVASCRIPT)").option("--dir <path>", "workflow directory (default: the current directory)").action(async (nodeId, type, opts, command) => {
14350
- const options = globalOptions(command);
14351
- const dir = await findWorkflowDir(process.cwd(), opts.dir);
14352
- const config = await readConfig(dir);
14353
- const location = locateNode(config, nodeId);
14354
- if (!location) {
14355
- throw new CliError(`No node "${nodeId}" in softr-workflows.jsonc.`);
14356
- }
14357
- const entry = findCatalogEntry(type, location.kind);
14358
- if (location.node.type === LOOP_TYPE && entry.type !== LOOP_TYPE) {
14359
- throw new CliError(`"${nodeId}" is a loop with a body; remove it instead of replacing it.`);
14360
- }
14361
- const title = location.node.title ?? entry.label;
14362
- const { inputs, codeFile } = await newNodeInputs(entry, nodeId, title, opts.lang, dir);
14363
- const node = { ...location.node, type: entry.type, version: entry.version, inputs };
14364
- delete node["outputSchema"];
14365
- await editConfig(dir, [{ path: location.path, value: node }]);
14366
- const oldCode = location.node.type === CUSTOM_CODE_TYPE && isFileRef(location.node.inputs?.["code"]) ? location.node.inputs?.["code"] : void 0;
14367
- const notes = [
14368
- oldCode && oldCode !== inputs["code"] ? `The previous code file (${oldCode}) is left in place; delete it when you no longer need it.` : "",
14369
- entry.kind === "trigger" && (config.triggers?.length ?? 0) > 1 ? "The service accepts one trigger per published workflow for now." : "",
14370
- `Fill inputs.${Object.keys(entry.template)[0] ?? "\u2026"} etc. in softr-workflows.jsonc (see "spec ${entry.type}"), then push and test ${nodeId}.`
14371
- ].filter(Boolean);
14372
- emitWithVersionNote(
14373
- options,
14374
- await readConfig(dir),
14375
- `Replaced "${nodeId}" (${location.node.type}) with ${entry.type}; paths kept.
14405
+ program3.command("replace").description("Give a node another type: keeps its id, title and paths; inputs start from the new type's template").argument("<nodeId>", "the trigger or action to change").argument(
14406
+ "<type>",
14407
+ 'the new type from "spec" (same kind: trigger or action), or any type the service knows (with --node-version)'
14408
+ ).option("--lang <language>", "for CUSTOM_CODE: JAVASCRIPT or PYTHON (default JAVASCRIPT)").option("--node-version <version>", "for a type outside the curated set: the node version the service runs").option("--dir <path>", "workflow directory (default: the current directory)").action(
14409
+ async (nodeId, type, opts, command) => {
14410
+ const options = globalOptions(command);
14411
+ const dir = await findWorkflowDir(process.cwd(), opts.dir);
14412
+ const config = await readConfig(dir);
14413
+ const location = locateNode(config, nodeId);
14414
+ if (!location) {
14415
+ throw new CliError(`No node "${nodeId}" in softr-workflows.jsonc.`);
14416
+ }
14417
+ const entry = resolveNodeType(type, { kind: location.kind, version: opts.nodeVersion });
14418
+ if (location.node.type === LOOP_TYPE && entry.type !== LOOP_TYPE) {
14419
+ throw new CliError(`"${nodeId}" is a loop with a body; remove it instead of replacing it.`);
14420
+ }
14421
+ const title = location.node.title ?? entry.label;
14422
+ const { inputs, codeFile } = await newNodeInputs(entry, nodeId, title, opts.lang, dir);
14423
+ const node = { ...location.node, type: entry.type, version: entry.version, inputs };
14424
+ delete node["outputSchema"];
14425
+ await editConfig(dir, [{ path: location.path, value: node }]);
14426
+ const oldCode = location.node.type === CUSTOM_CODE_TYPE && isFileRef(location.node.inputs?.["code"]) ? location.node.inputs?.["code"] : void 0;
14427
+ const notes = [
14428
+ oldCode && oldCode !== inputs["code"] ? `The previous code file (${oldCode}) is left in place; delete it when you no longer need it.` : "",
14429
+ entry.kind === "trigger" && (config.triggers?.length ?? 0) > 1 ? "The service accepts one trigger per published workflow for now." : "",
14430
+ entry.curated ? `Fill inputs.${Object.keys(entry.template)[0] ?? "\u2026"} etc. in softr-workflows.jsonc (see "spec ${entry.type}"), then push and test ${nodeId}.` : `Fill its inputs in softr-workflows.jsonc from its specification ("spec ${entry.type}" says where to find it), then push and test ${nodeId}.`
14431
+ ].filter(Boolean);
14432
+ emitWithVersionNote(
14433
+ options,
14434
+ await readConfig(dir),
14435
+ `Replaced "${nodeId}" (${location.node.type}) with ${entry.type}; paths kept.
14376
14436
  ${notes.join("\n")}`,
14377
- {
14378
- id: nodeId,
14379
- from: location.node.type,
14380
- to: entry.type,
14381
- codeFile: codeFile ? path5.relative(process.cwd(), codeFile) : null
14382
- }
14383
- );
14384
- });
14437
+ {
14438
+ id: nodeId,
14439
+ from: location.node.type,
14440
+ to: entry.type,
14441
+ curated: entry.curated,
14442
+ codeFile: codeFile ? path5.relative(process.cwd(), codeFile) : null
14443
+ }
14444
+ );
14445
+ }
14446
+ );
14385
14447
  program3.command("remove").description("Remove a node and every path that leads to or from it (a loop goes with its body)").argument("<nodeId>", "the trigger or action to remove").option("--dir <path>", "workflow directory (default: the current directory)").action(async (nodeId, opts, command) => {
14386
14448
  const options = globalOptions(command);
14387
14449
  const dir = await findWorkflowDir(process.cwd(), opts.dir);
@@ -14430,6 +14492,15 @@ function slug(title, fallback = "workflow") {
14430
14492
  }
14431
14493
 
14432
14494
  // src/commands/add.ts
14495
+ function parseKind(kind) {
14496
+ if (kind === void 0) {
14497
+ return void 0;
14498
+ }
14499
+ if (kind === "trigger" || kind === "action") {
14500
+ return kind;
14501
+ }
14502
+ throw new CliError(`--kind takes trigger or action, not "${kind}".`);
14503
+ }
14433
14504
  function uniqueId(base, taken) {
14434
14505
  if (!taken.has(base)) {
14435
14506
  return base;
@@ -14465,11 +14536,14 @@ function defaultAfter(config, loopId) {
14465
14536
  );
14466
14537
  }
14467
14538
  function registerAdd(program3) {
14468
- program3.command("add").description("Add an action after an existing node, or a trigger; custom code gets its own file under actions/").argument("<type>", 'node type from "spec", e.g. CUSTOM_CODE or WEBHOOK').option("--id <id>", "node id, readable and unique (default: derived from the type)").option("--title <title>", "node title shown in the studio (default: the type label)").option("--after <nodeId>", "actions: the node the new one follows (default: the only node without a successor)").option("--to <nodeId>", "triggers: an action the new trigger leads to").option("--in-loop <loopId>", "put the node inside this loop body").option("--lang <language>", "for CUSTOM_CODE: JAVASCRIPT or PYTHON (default JAVASCRIPT)").option("--dir <path>", "workflow directory (default: the current directory)").action(async (type, opts, command) => {
14539
+ program3.command("add").description("Add an action after an existing node, or a trigger; custom code gets its own file under actions/").argument(
14540
+ "<type>",
14541
+ 'node type from "spec", e.g. CUSTOM_CODE or WEBHOOK, or any type the service knows (with --node-version)'
14542
+ ).option("--id <id>", "node id, readable and unique (default: derived from the type)").option("--title <title>", "node title shown in the studio (default: the type label)").option("--after <nodeId>", "actions: the node the new one follows (default: the only node without a successor)").option("--to <nodeId>", "triggers: an action the new trigger leads to").option("--in-loop <loopId>", "put the node inside this loop body").option("--lang <language>", "for CUSTOM_CODE: JAVASCRIPT or PYTHON (default JAVASCRIPT)").option("--node-version <version>", "for a type outside the curated set: the node version the service runs").option("--kind <kind>", "for a type outside the curated set: trigger or action (default action)").option("--dir <path>", "workflow directory (default: the current directory)").action(async (type, opts, command) => {
14469
14543
  const options = globalOptions(command);
14470
14544
  const dir = await findWorkflowDir(process.cwd(), opts.dir);
14471
14545
  const config = await readConfig(dir);
14472
- const entry = findCatalogEntry(type);
14546
+ const entry = resolveNodeType(type, { kind: parseKind(opts.kind), version: opts.nodeVersion });
14473
14547
  const nodes = [...config.triggers ?? [], ...allActions(config.actions)];
14474
14548
  const taken = new Set(nodes.map((node2) => node2.id));
14475
14549
  const id = opts.id ?? uniqueId(entry.kind === "trigger" ? "trigger" : slug(entry.label), taken);
@@ -14509,6 +14583,7 @@ ${notes.join("\n")}`,
14509
14583
  id,
14510
14584
  kind: "trigger",
14511
14585
  type: entry.type,
14586
+ curated: entry.curated,
14512
14587
  to: opts.to ?? null
14513
14588
  }
14514
14589
  );
@@ -14533,6 +14608,7 @@ ${notes.join("\n")}`,
14533
14608
  const next = [
14534
14609
  codeFile ? `Write the function in ${path6.relative(process.cwd(), codeFile)} and map its inputs under inputs.inputData in softr-workflows.jsonc.` : "",
14535
14610
  entry.type === "BRANCH" || entry.type === "FILTER" ? `Add the condition(s) on the path(s) leaving it in softr-workflows.jsonc (see "spec ${entry.type}").` : "",
14611
+ entry.curated ? "" : `Fill its inputs in softr-workflows.jsonc from its specification ("spec ${entry.type}" says where to find it).`,
14536
14612
  `Then: softr workflows push, softr workflows test ${id}.`
14537
14613
  ].filter(Boolean);
14538
14614
  emitWithVersionNote(
@@ -14544,6 +14620,7 @@ ${next.join("\n")}`,
14544
14620
  id,
14545
14621
  kind: "action",
14546
14622
  type: entry.type,
14623
+ curated: entry.curated,
14547
14624
  after,
14548
14625
  loopId: opts.inLoop ?? null,
14549
14626
  codeFile: codeFile ? path6.relative(process.cwd(), codeFile) : null
@@ -17697,7 +17774,8 @@ function renderList() {
17697
17774
  }
17698
17775
  return [
17699
17776
  table.toString(),
17700
- "This CLI offers only these node types on purpose: code-first workflows use custom code for everything else.",
17777
+ "These are the curated types: documented here, with a template and an example each. Any other node type the",
17778
+ 'workflows service knows can be written into softr-workflows.jsonc as well; "softr workflows spec <TYPE>" says how.',
17701
17779
  'Run "softr workflows spec <TYPE>" for the inputs, output and an example of one type.'
17702
17780
  ].join("\n");
17703
17781
  }
@@ -17756,7 +17834,9 @@ ${JSON.stringify(extra.json, null, 2)}`);
17756
17834
  return blocks.join("\n\n");
17757
17835
  }
17758
17836
  function registerSpec(program3) {
17759
- program3.command("spec").description("List the node types this CLI offers, or show the inputs, output and an example of one").argument("[type]", "a node type such as CUSTOM_CODE").action((type, _opts, command) => {
17837
+ program3.command("spec").description(
17838
+ "List the curated node types, or show the inputs, output and an example of one; any other type: how to add it"
17839
+ ).argument("[type]", "a node type such as CUSTOM_CODE").action((type, _opts, command) => {
17760
17840
  const options = globalOptions(command);
17761
17841
  if (!type) {
17762
17842
  emit(options, renderList(), {
@@ -17770,8 +17850,13 @@ function registerSpec(program3) {
17770
17850
  });
17771
17851
  return;
17772
17852
  }
17773
- const entry = findCatalogEntry(type);
17774
- emit(options, renderEntry(entry), { ...entry, testable: isTestableType(entry.type) });
17853
+ const entry = catalogEntryOrNull(type);
17854
+ if (!entry) {
17855
+ const guidance = uncuratedGuidance(type);
17856
+ emit(options, guidance, { type: type.trim().toUpperCase(), curated: false, guidance });
17857
+ return;
17858
+ }
17859
+ emit(options, renderEntry(entry), { ...entry, curated: true, testable: isTestableType(entry.type) });
17775
17860
  });
17776
17861
  }
17777
17862
 
@@ -17998,7 +18083,7 @@ Pass --workspace <id> to a command, or set SOFTR_WORKSPACE_ID, to work in one of
17998
18083
  // package.json
17999
18084
  var package_default = {
18000
18085
  name: "@softr/cli",
18001
- version: "0.1.6",
18086
+ version: "0.1.7-se22117.20",
18002
18087
  description: "The Softr command line: build, test and publish Softr workflows from local files, manage Softr Databases; more Softr APIs to come",
18003
18088
  keywords: [
18004
18089
  "softr",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@softr/cli",
3
- "version": "0.1.6",
3
+ "version": "0.1.7-se22117.20",
4
4
  "description": "The Softr command line: build, test and publish Softr workflows from local files, manage Softr Databases; more Softr APIs to come",
5
5
  "keywords": [
6
6
  "softr",