@softr/cli 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +93 -81
  2. package/dist/cli.mjs +377 -255
  3. package/package.json +3 -4
package/dist/cli.mjs CHANGED
@@ -1745,7 +1745,7 @@ var require_table = __commonJS({
1745
1745
  var debug = require_debug();
1746
1746
  var utils = require_utils();
1747
1747
  var tableLayout = require_layout_manager();
1748
- var Table6 = class extends Array {
1748
+ var Table7 = class extends Array {
1749
1749
  constructor(opts) {
1750
1750
  super();
1751
1751
  const options = utils.mergeOptions(opts);
@@ -1820,7 +1820,7 @@ var require_table = __commonJS({
1820
1820
  return str[0].length;
1821
1821
  }
1822
1822
  };
1823
- Table6.reset = () => debug.reset();
1823
+ Table7.reset = () => debug.reset();
1824
1824
  function doDraw(row, lineNum, result) {
1825
1825
  let line = [];
1826
1826
  row.forEach(function(cell) {
@@ -1829,7 +1829,7 @@ var require_table = __commonJS({
1829
1829
  let str = line.join("");
1830
1830
  if (str.length) result.push(str);
1831
1831
  }
1832
- module.exports = Table6;
1832
+ module.exports = Table7;
1833
1833
  }
1834
1834
  });
1835
1835
 
@@ -5278,9 +5278,17 @@ function executionOrder(config) {
5278
5278
  }
5279
5279
  };
5280
5280
  for (const trigger of config.triggers ?? []) {
5281
- add({ id: trigger.id, type: trigger.type, kind: "trigger", title: trigger.title, testable: true, reachable: true });
5281
+ add({
5282
+ id: trigger.id,
5283
+ type: trigger.type,
5284
+ kind: "trigger",
5285
+ title: trigger.title,
5286
+ testable: true,
5287
+ loopPath: [],
5288
+ reachable: true
5289
+ });
5282
5290
  }
5283
- const collect = (actions, loopId) => {
5291
+ const collect = (actions, loopPath) => {
5284
5292
  for (const action of actions ?? []) {
5285
5293
  add({
5286
5294
  id: action.id,
@@ -5288,17 +5296,18 @@ function executionOrder(config) {
5288
5296
  kind: "action",
5289
5297
  title: action.title,
5290
5298
  testable: !NOT_TESTABLE_TYPES.has(action.type),
5291
- loopId,
5299
+ loopId: loopPath.at(-1),
5300
+ loopPath,
5292
5301
  reachable: false
5293
5302
  });
5294
5303
  if (action.type === LOOP_TYPE) {
5295
5304
  const inputs = action.inputs;
5296
5305
  pathSets.push(inputs?.paths ?? []);
5297
- collect(inputs?.actions, action.id);
5306
+ collect(inputs?.actions, [...loopPath, action.id]);
5298
5307
  }
5299
5308
  }
5300
5309
  };
5301
- collect(config.actions);
5310
+ collect(config.actions, []);
5302
5311
  const outgoing = new Map(appearance.map((id) => [id, []]));
5303
5312
  const indegree = new Map(appearance.map((id) => [id, 0]));
5304
5313
  for (const path13 of pathSets.flat()) {
@@ -5342,6 +5351,9 @@ function executionOrder(config) {
5342
5351
  }
5343
5352
  return [...ordered.filter((node) => node.reachable), ...ordered.filter((node) => !node.reachable)];
5344
5353
  }
5354
+ function hierarchyPath(node, iteration = 0) {
5355
+ return [...node.loopPath.flatMap((loopId) => [loopId, String(iteration)]), node.id].join("/");
5356
+ }
5345
5357
 
5346
5358
  // src/catalog/catalog.json
5347
5359
  var catalog_default = [
@@ -5698,7 +5710,7 @@ var catalog_default = [
5698
5710
  integrations: {
5699
5711
  type: "array",
5700
5712
  required: false,
5701
- description: 'Workspace integrations the code may call through fetch: [{"alias": "crm", "integrationId": "<uuid>"}]. A call to a host of an attached integration carries its credentials (a REST_API integration only when the call names its alias); the alias ([A-Za-z_][A-Za-z0-9_]*, unique in the node) names one in fetch(url, { integration: "crm" }) when two share a host. Integration ids: `softr-workflows integrations` lists them, or the studio (Workspace settings \u2192 Integrations, or the URL of the integration).',
5713
+ description: 'Workspace integrations the code may call through fetch: [{"alias": "crm", "integrationId": "<uuid>"}]. A call to a host of an attached integration carries its credentials (a REST_API integration only when the call names its alias); the alias ([A-Za-z_][A-Za-z0-9_]*, unique in the node) names one in fetch(url, { integration: "crm" }) when two share a host. Integration ids: `softr integrations` lists them, or the studio (Workspace settings \u2192 Integrations, or the URL of the integration).',
5702
5714
  example: [
5703
5715
  {
5704
5716
  alias: "crm",
@@ -6088,7 +6100,7 @@ var SoftrApiError = class extends Error {
6088
6100
  };
6089
6101
  function hintFor(status) {
6090
6102
  if (status === 401 || status === 403) {
6091
- return "The token was not accepted: revoked, from another environment, missing access to this workflow, or its statements do not grant this action. Run: softr-workflows login";
6103
+ return "The token was not accepted: revoked, from another environment, missing access to this workflow, or its statements do not grant this action. Run: softr login";
6092
6104
  }
6093
6105
  if (status === 409) {
6094
6106
  return "The workflow changed on the server since the last pull. Pull again with --force, then re-apply your edits.";
@@ -7588,7 +7600,7 @@ async function readConfig(dir) {
7588
7600
  if (error.code === "ENOENT") {
7589
7601
  throw new CliError(
7590
7602
  `No ${CONFIG_FILE_NAME} in ${dir}.`,
7591
- 'Run "softr-workflows pull <workflowId>" here, or pass --dir.'
7603
+ 'Run "softr workflows pull <workflowId>" here, or pass --dir.'
7592
7604
  );
7593
7605
  }
7594
7606
  throw error;
@@ -7619,7 +7631,7 @@ async function findWorkflowDir(cwd, explicitDir) {
7619
7631
  return cwd;
7620
7632
  }
7621
7633
  const candidates = await listWorkflowDirs(cwd);
7622
- const hint = candidates.length ? `Workflows found in: ${candidates.map((dir) => path2.relative(cwd, dir)).join(", ")}. Pass --dir <name> or cd into one.` : 'Run "softr-workflows pull <workflowId>" to create one.';
7634
+ const hint = candidates.length ? `Workflows found in: ${candidates.map((dir) => path2.relative(cwd, dir)).join(", ")}. Pass --dir <name> or cd into one.` : 'Run "softr workflows pull <workflowId>" to create one.';
7623
7635
  throw new CliError(`No ${CONFIG_FILE_NAME} in ${cwd}.`, hint);
7624
7636
  }
7625
7637
  async function listWorkflowDirs(cwd) {
@@ -7641,6 +7653,79 @@ async function editConfig(dir, edits) {
7641
7653
  `);
7642
7654
  }
7643
7655
 
7656
+ // src/project/versions.ts
7657
+ function compareVersions(a, b) {
7658
+ const pa = a.split(".").map((part) => Number.parseInt(part, 10) || 0);
7659
+ const pb = b.split(".").map((part) => Number.parseInt(part, 10) || 0);
7660
+ for (let i2 = 0; i2 < Math.max(pa.length, pb.length); i2++) {
7661
+ const diff = (pa[i2] ?? 0) - (pb[i2] ?? 0);
7662
+ if (diff !== 0) {
7663
+ return diff;
7664
+ }
7665
+ }
7666
+ return 0;
7667
+ }
7668
+ var CURRENT = new Map(CATALOG.map((entry) => [entry.type, entry.version]));
7669
+ function outdatedNodes(config) {
7670
+ const outdated = [];
7671
+ for (const node of [...config.triggers ?? [], ...allActions(config.actions)]) {
7672
+ const current2 = CURRENT.get(node.type);
7673
+ if (!current2 || !node.version || compareVersions(node.version, current2) >= 0) {
7674
+ continue;
7675
+ }
7676
+ outdated.push({
7677
+ id: node.id,
7678
+ type: node.type,
7679
+ version: node.version,
7680
+ current: current2,
7681
+ upgradable: upgradeOf(node) !== void 0
7682
+ });
7683
+ }
7684
+ return outdated;
7685
+ }
7686
+ function upgradeOf(node) {
7687
+ const current2 = CURRENT.get(node.type);
7688
+ if (!current2 || !node.version || compareVersions(node.version, current2) >= 0) {
7689
+ return void 0;
7690
+ }
7691
+ if (node.type === CUSTOM_CODE_TYPE) {
7692
+ const inputs = node.inputs ?? {};
7693
+ const ordered = {};
7694
+ for (const key of ["language", "inputData", "integrations", "code"]) {
7695
+ if (key === "integrations") {
7696
+ ordered[key] = inputs[key] ?? [];
7697
+ } else if (inputs[key] !== void 0) {
7698
+ ordered[key] = inputs[key];
7699
+ }
7700
+ }
7701
+ for (const [key, value] of Object.entries(inputs)) {
7702
+ if (!(key in ordered)) {
7703
+ ordered[key] = value;
7704
+ }
7705
+ }
7706
+ return { ...node, version: current2, inputs: ordered };
7707
+ }
7708
+ return void 0;
7709
+ }
7710
+ function versionNote(config) {
7711
+ const outdated = outdatedNodes(config);
7712
+ if (!outdated.length) {
7713
+ return void 0;
7714
+ }
7715
+ const lines = outdated.map(
7716
+ (node) => ` ${node.id}: ${node.type} ${node.version} \u2192 ${node.current}${node.upgradable ? "" : ` (no automatic upgrade; see "spec ${node.type}")`}`
7717
+ );
7718
+ const hint = outdated.some((node) => node.upgradable) ? '\nRun "softr workflows upgrade" to move them to the current version.' : "";
7719
+ return `Nodes on an older version:
7720
+ ${lines.join("\n")}${hint}`;
7721
+ }
7722
+ function emitWithVersionNote(options, config, human, json) {
7723
+ const note = versionNote(config);
7724
+ emit(options, note ? `${human}
7725
+
7726
+ ${note}` : human, { ...json, versionNote: note ?? null });
7727
+ }
7728
+
7644
7729
  // src/commands/edit.ts
7645
7730
  import { access as access2, mkdir as mkdir3, writeFile as writeFile4 } from "node:fs/promises";
7646
7731
  import path5 from "node:path";
@@ -13591,6 +13676,36 @@ function renderPyType(type, depth = 0) {
13591
13676
  }
13592
13677
 
13593
13678
  // src/project/code-wrapper.ts
13679
+ function attachedIntegrations(inputs) {
13680
+ const value = inputs?.["integrations"];
13681
+ if (!Array.isArray(value)) {
13682
+ return [];
13683
+ }
13684
+ return value.flatMap(
13685
+ (entry) => entry && typeof entry === "object" && typeof entry.alias === "string" ? [
13686
+ {
13687
+ alias: entry.alias,
13688
+ integrationId: String(entry.integrationId ?? "")
13689
+ }
13690
+ ] : []
13691
+ );
13692
+ }
13693
+ function integrationLines(header) {
13694
+ if (!hasFetch(header.version)) {
13695
+ return [];
13696
+ }
13697
+ const attached = header.integrations ?? [];
13698
+ if (!attached.length) {
13699
+ return [
13700
+ 'No integrations attached: add { alias, integrationId } entries under inputs.integrations in softr-workflows.jsonc (ids from "softr integrations").'
13701
+ ];
13702
+ }
13703
+ return [
13704
+ "Integrations attached to this node; fetch authenticates a call to their own API hosts by itself:",
13705
+ ...attached.map((item) => ` ${item.alias}: ${item.integrationId}`),
13706
+ `Name one with fetch(url, { integration: '${attached[0].alias}' }) only for a REST API integration (it has no host of its own) or when two attached integrations share a host.`
13707
+ ];
13708
+ }
13594
13709
  var PLACEHOLDER_IN_CODE = /\{\{?[A-Za-z]+[^{}]*:::/;
13595
13710
  function parseLanguage(value, nodeId) {
13596
13711
  if (value === "JAVASCRIPT" || value === "PYTHON") {
@@ -13613,17 +13728,6 @@ var FETCH_SINCE = "1.2.0";
13613
13728
  function hasFetch(version2) {
13614
13729
  return compareVersions(version2, FETCH_SINCE) >= 0;
13615
13730
  }
13616
- function compareVersions(a, b) {
13617
- const pa = a.split(".").map((part) => Number.parseInt(part, 10) || 0);
13618
- const pb = b.split(".").map((part) => Number.parseInt(part, 10) || 0);
13619
- for (let i2 = 0; i2 < Math.max(pa.length, pb.length); i2++) {
13620
- const diff = (pa[i2] ?? 0) - (pb[i2] ?? 0);
13621
- if (diff !== 0) {
13622
- return diff;
13623
- }
13624
- }
13625
- return 0;
13626
- }
13627
13731
  var JS_RULES = [
13628
13732
  ...RULES,
13629
13733
  "HTTP calls: `await fetch(url, { method, headers, body, integration })`. An attached integration (inputs.integrations) authenticates calls to its own API hosts; a REST_API integration authenticates only a call that names its alias. No `import`/`require`. An undefined return is reported as null."
@@ -13650,8 +13754,8 @@ function rules(language, version2) {
13650
13754
  `This node runs CUSTOM_CODE ${version2}: synchronous, no fetch, inputs.integrations is ignored. Set "version": "1.2.0" on this node in softr-workflows.jsonc to use them.`
13651
13755
  ];
13652
13756
  }
13653
- var HEADER_START = "softr-workflows: generated header - regenerated on pull, do not edit by hand";
13654
- var HEADER_END = "softr-workflows: end of generated header";
13757
+ var HEADER_START = "softr: generated header - regenerated on pull, do not edit by hand";
13758
+ var HEADER_END = "softr: end of generated header";
13655
13759
  var MAX_EXAMPLE = 60;
13656
13760
  function describe(header) {
13657
13761
  return `Softr Workflows custom code for node "${header.nodeId}"${header.title ? ` (${header.title})` : ""}.`;
@@ -13693,6 +13797,7 @@ function wrapCode(language, body, header) {
13693
13797
  ...rules("JAVASCRIPT", header.version).map((rule) => ` * ${rule}`),
13694
13798
  " */",
13695
13799
  ...typedef,
13800
+ ...integrationLines(header).map((line) => `// ${line}`),
13696
13801
  `// ${HEADER_END}`,
13697
13802
  `/** @param {${paramType}} inputData */`,
13698
13803
  `export default ${hasFetch(header.version) ? "async " : ""}function (inputData) {`,
@@ -13712,6 +13817,7 @@ function wrapCode(language, body, header) {
13712
13817
  ...rules("PYTHON", header.version).map((rule) => `# ${rule}`),
13713
13818
  "#",
13714
13819
  ...inputs,
13820
+ ...integrationLines(header).map((line) => `# ${line}`),
13715
13821
  `# ${HEADER_END}`,
13716
13822
  "def main(inputData):",
13717
13823
  ...lines.length ? lines.map((line) => line === "" ? "" : ` ${line}`) : [" pass"],
@@ -13948,7 +14054,7 @@ function describePlaceholder(placeholder, raw, config, outputs, depth) {
13948
14054
  }
13949
14055
  }
13950
14056
  function untested(nodeId) {
13951
- return `node "${nodeId}" has no test output yet (run: softr-workflows test ${nodeId})`;
14057
+ return `node "${nodeId}" has no test output yet (run: softr workflows test ${nodeId})`;
13952
14058
  }
13953
14059
  function describeOutputRef(nodeId, path13, source, outputs) {
13954
14060
  const segments = parsePath(path13);
@@ -14076,7 +14182,8 @@ async function externalizeCode(config, baseDir, outputs = {}) {
14076
14182
  nodeId: node.id,
14077
14183
  title: node.title,
14078
14184
  version: nodeVersion(node),
14079
- inputs: describeInputData(node, config, outputs)
14185
+ inputs: describeInputData(node, config, outputs),
14186
+ integrations: attachedIntegrations(node.inputs)
14080
14187
  })
14081
14188
  );
14082
14189
  node.inputs["code"] = toFileRef(relative);
@@ -14131,7 +14238,8 @@ async function refreshCodeHeaders(config, baseDir, outputs) {
14131
14238
  nodeId: node.id,
14132
14239
  title: node.title,
14133
14240
  version: nodeVersion(node),
14134
- inputs: describeInputData(node, config, outputs)
14241
+ inputs: describeInputData(node, config, outputs),
14242
+ integrations: attachedIntegrations(node.inputs)
14135
14243
  });
14136
14244
  if (next !== source) {
14137
14245
  await writeFile3(file, next);
@@ -14221,8 +14329,9 @@ function registerEdit(program3) {
14221
14329
  entry.kind === "trigger" && (config.triggers?.length ?? 0) > 1 ? "The service accepts one trigger per published workflow for now." : "",
14222
14330
  `Fill inputs.${Object.keys(entry.template)[0] ?? "\u2026"} etc. in softr-workflows.jsonc (see "spec ${entry.type}"), then push and test ${nodeId}.`
14223
14331
  ].filter(Boolean);
14224
- emit(
14332
+ emitWithVersionNote(
14225
14333
  options,
14334
+ await readConfig(dir),
14226
14335
  `Replaced "${nodeId}" (${location.node.type}) with ${entry.type}; paths kept.
14227
14336
  ${notes.join("\n")}`,
14228
14337
  {
@@ -14261,8 +14370,9 @@ ${notes.join("\n")}`,
14261
14370
  codeFiles.length ? `Code files left in place: ${codeFiles.join(", ")}. Delete them when you no longer need them.` : "",
14262
14371
  location.kind === "trigger" && (config.triggers?.length ?? 1) <= 1 ? "The workflow has no trigger now; add one before publishing." : ""
14263
14372
  ].filter(Boolean);
14264
- emit(
14373
+ emitWithVersionNote(
14265
14374
  options,
14375
+ await readConfig(dir),
14266
14376
  `Removed ${removedNodes.map((id) => `"${id}"`).join(", ")} and ${doomed.length} path(s).${notes.length ? `
14267
14377
  ${notes.join("\n")}` : ""}`,
14268
14378
  {
@@ -14291,9 +14401,8 @@ function listWorkflows(client, workspaceId) {
14291
14401
  const query = new URLSearchParams({ workspaceId, limit: "500" });
14292
14402
  return apiRequest(client, "GET", `/v1/workflows?${query}`);
14293
14403
  }
14294
- function testAction(client, workflowId, actionId, mode, iterationIndex) {
14295
- const query = iterationIndex === void 0 ? "" : `?${new URLSearchParams({ iterationIndex: String(iterationIndex) })}`;
14296
- return apiRequest(client, "POST", `/v1/workflows/${workflowId}/outputs/actions/${actionId}/${mode}${query}`, {});
14404
+ function testAction(client, workflowId, hierarchyPath2, mode) {
14405
+ return apiRequest(client, "POST", `/v1/workflows/${workflowId}/outputs/${mode}/actions/${hierarchyPath2}`, {});
14297
14406
  }
14298
14407
  function testTrigger(client, workflowId, triggerId) {
14299
14408
  return apiRequest(client, "POST", `/v1/workflows/${workflowId}/outputs/triggers/${triggerId}/test`, {});
@@ -14320,18 +14429,8 @@ async function getEnabledVersion(client, workflowId) {
14320
14429
  throw error;
14321
14430
  }
14322
14431
  }
14323
- async function getWorkflowReadiness(client, workflowId) {
14324
- try {
14325
- return await apiRequest(client, "GET", `/v1/workflows/${workflowId}/readiness`);
14326
- } catch (error) {
14327
- if (error instanceof SoftrApiError && (error.status === 404 || isUnknownRouteAnswer(error))) {
14328
- return null;
14329
- }
14330
- throw error;
14331
- }
14332
- }
14333
- function isUnknownRouteAnswer(error) {
14334
- return error.status === 500 && error.body.includes("ApiErrorMessages.INTERNAL_SERVER_ERROR");
14432
+ function getWorkflowReadiness(client, workflowId) {
14433
+ return apiRequest(client, "GET", `/v1/workflows/${workflowId}/readiness`);
14335
14434
  }
14336
14435
 
14337
14436
  // src/config/credentials.ts
@@ -14341,7 +14440,7 @@ import path6 from "node:path";
14341
14440
 
14342
14441
  // src/api/oauth.ts
14343
14442
  import { createHash as createHash2, randomBytes } from "node:crypto";
14344
- var OAUTH_CLIENT_ID_ENV_VAR = "SOFTR_WORKFLOWS_OAUTH_CLIENT_ID";
14443
+ var OAUTH_CLIENT_ID_ENV_VAR = "SOFTR_OAUTH_CLIENT_ID";
14345
14444
  var DEFAULT_OAUTH_CLIENT_ID = "74341326-0061-4f0e-965c-a5aa14a9a89a";
14346
14445
  function oauthClientId() {
14347
14446
  return process.env[OAUTH_CLIENT_ID_ENV_VAR] || DEFAULT_OAUTH_CLIENT_ID;
@@ -14379,7 +14478,7 @@ async function tokenRequest(target, form) {
14379
14478
  `POST ${path13} failed with status ${response.status}${text ? `: ${text}` : ""}`,
14380
14479
  response.status,
14381
14480
  text,
14382
- form["grant_type"] === "refresh_token" ? "The OAuth grant is gone (revoked or older than 30 days). Run: softr-workflows login" : void 0
14481
+ form["grant_type"] === "refresh_token" ? "The OAuth grant is gone (revoked or older than 30 days). Run: softr login" : void 0
14383
14482
  );
14384
14483
  }
14385
14484
  const tokens = await response.json();
@@ -14403,8 +14502,8 @@ function refreshTokens(target, refreshToken) {
14403
14502
 
14404
14503
  // src/config/target.ts
14405
14504
  var API_URL_ENV_VAR = "SOFTR_WORKFLOWS_API_URL";
14406
- var STUDIO_API_URL_ENV_VAR = "SOFTR_WORKFLOWS_STUDIO_API_URL";
14407
- var STUDIO_URL_ENV_VAR = "SOFTR_WORKFLOWS_STUDIO_URL";
14505
+ var STUDIO_API_URL_ENV_VAR = "SOFTR_STUDIO_API_URL";
14506
+ var STUDIO_URL_ENV_VAR = "SOFTR_STUDIO_URL";
14408
14507
  var DEFAULT_API_URL = "https://workflows-api.softr.io";
14409
14508
  var DEFAULT_STUDIO_API_URL = "https://studio-api.softr.io";
14410
14509
  var DEFAULT_STUDIO_URL = "https://studio.softr.io";
@@ -14429,7 +14528,7 @@ function resolveTarget(remembered) {
14429
14528
  };
14430
14529
  }
14431
14530
  function targetHost(target) {
14432
- return new URL(target.apiUrl).host;
14531
+ return new URL(target.studioApiUrl).host;
14433
14532
  }
14434
14533
 
14435
14534
  // src/config/tokens.ts
@@ -14442,12 +14541,12 @@ function tokenKind(token) {
14442
14541
  }
14443
14542
  throw new CliError(
14444
14543
  "The token is neither a personal API token (pat_...) nor an OAuth access token (oaa_...).",
14445
- 'Create a personal API token in the studio and paste the whole value, or run "softr-workflows login" without --token to log in through the browser.'
14544
+ 'Create a personal API token in the studio and paste the whole value, or run "softr login" without --token to log in through the browser.'
14446
14545
  );
14447
14546
  }
14448
14547
 
14449
14548
  // src/config/credentials.ts
14450
- var TOKEN_ENV_VAR = "SOFTR_WORKFLOWS_TOKEN";
14549
+ var TOKEN_ENV_VAR = "SOFTR_TOKEN";
14451
14550
  var REFRESH_MARGIN_MS = 5 * 60 * 1e3;
14452
14551
  function credentialsPath() {
14453
14552
  return path6.join(homedir(), ".softr", "credentials.json");
@@ -14518,7 +14617,7 @@ async function authenticatedClient(target) {
14518
14617
  }
14519
14618
  const stored = await storedToken(target);
14520
14619
  if (!stored) {
14521
- throw new CliError(`No token stored for ${targetHost(target)}.`, "Run: softr-workflows login");
14620
+ throw new CliError(`No token stored for ${targetHost(target)}.`, "Run: softr login");
14522
14621
  }
14523
14622
  const kind = stored.kind ?? tokenKind(stored.token);
14524
14623
  const expiresAt = stored.expiresAt ? new Date(stored.expiresAt) : void 0;
@@ -14528,7 +14627,7 @@ async function authenticatedClient(target) {
14528
14627
  if (expiresAt && expiresAt < /* @__PURE__ */ new Date()) {
14529
14628
  throw new CliError(
14530
14629
  `The stored token for ${targetHost(target)} expired at ${stored.expiresAt}.`,
14531
- "Run: softr-workflows login"
14630
+ "Run: softr login"
14532
14631
  );
14533
14632
  }
14534
14633
  return { target, token: stored.token, kind };
@@ -14545,7 +14644,7 @@ async function resolveTokenOwner(client) {
14545
14644
  if (error instanceof SoftrApiError && error.status === 404) {
14546
14645
  throw new CliError(
14547
14646
  "The studio does not know this token: revoked, expired, or from another environment.",
14548
- 'Create a new personal API token in the studio, or run "softr-workflows login" for the browser login.'
14647
+ 'Create a new personal API token in the studio, or run "softr login" for the browser login.'
14549
14648
  );
14550
14649
  }
14551
14650
  throw error;
@@ -14663,10 +14762,10 @@ function workflowTable(rows) {
14663
14762
  }
14664
14763
  return table.toString();
14665
14764
  }
14666
- function renderWorkflows({ workspaces, workflows, failures }) {
14765
+ function renderWorkflows({ workspaces, workflows: workflows2, failures }) {
14667
14766
  const blocks = [];
14668
14767
  for (const workspace of workspaces) {
14669
- const rows = workflows.filter((row) => row.workspaceId === workspace.id);
14768
+ const rows = workflows2.filter((row) => row.workspaceId === workspace.id);
14670
14769
  const heading = `Workspace: ${workspace.name} (${workspace.id})`;
14671
14770
  blocks.push(rows.length ? `${heading}
14672
14771
  ${workflowTable(rows)}` : `${heading}
@@ -14689,11 +14788,11 @@ async function emitWorkflowList(options, client, workspaceId, human) {
14689
14788
  emit(options, [human, renderWorkflows(listing)].filter(Boolean).join("\n\n"), { ...listing });
14690
14789
  }
14691
14790
  function registerList(program3) {
14692
- program3.command("list").description("List the workflows you can access, grouped by workspace").option("--workspace <id>", "only this workspace").action(async (opts, command) => {
14791
+ program3.command("list").description("List the workflows you can access, grouped by workspace").option("--workspace <id>", "only this workspace (default: $SOFTR_WORKSPACE_ID when set)").action(async (opts, command) => {
14693
14792
  const options = globalOptions(command);
14694
14793
  const target = resolveTarget(await readState(process.cwd()));
14695
14794
  const client = await authenticatedClient(target);
14696
- await emitWorkflowList(options, client, opts.workspace);
14795
+ await emitWorkflowList(options, client, opts.workspace ?? process.env["SOFTR_WORKSPACE_ID"]);
14697
14796
  });
14698
14797
  }
14699
14798
 
@@ -14960,7 +15059,7 @@ function registerPull(program3) {
14960
15059
  options,
14961
15060
  client,
14962
15061
  void 0,
14963
- "Pick a workflow and run: softr-workflows pull <workflowId>"
15062
+ "Pick a workflow and run: softr workflows pull <workflowId>"
14964
15063
  );
14965
15064
  return;
14966
15065
  }
@@ -14991,8 +15090,9 @@ Sample outputs not available, inputData types are unknown: ${error.message}`;
14991
15090
  outputsFile = await writeStateFile(dir, "outputs.json", rows);
14992
15091
  }
14993
15092
  const files = [configFile, ...codeFiles].map((file) => path8.relative(cwd, file));
14994
- emit(
15093
+ emitWithVersionNote(
14995
15094
  options,
15095
+ config,
14996
15096
  `Pulled "${workflow.title}" (version ${workflow.version}, draft ${workflow.draft}, ${workflow.enabled ? "enabled" : "not enabled"}).
14997
15097
  ` + files.map((file) => ` ${file}`).join("\n") + outputsNote,
14998
15098
  {
@@ -15079,10 +15179,11 @@ function registerAdd(program3) {
15079
15179
  const notes = [
15080
15180
  (config.triggers?.length ?? 0) >= 1 ? "The service accepts one trigger per published workflow for now; multi-trigger support is coming." : "",
15081
15181
  opts.to ? "" : `Connect it with a path to the first action (or pass --to next time).`,
15082
- `Then: softr-workflows push, softr-workflows test ${id}.`
15182
+ `Then: softr workflows push, softr workflows test ${id}.`
15083
15183
  ].filter(Boolean);
15084
- emit(
15184
+ emitWithVersionNote(
15085
15185
  options,
15186
+ config,
15086
15187
  `Added ${entry.type} trigger "${id}"${opts.to ? ` leading to "${opts.to}"` : ""}.
15087
15188
  ${notes.join("\n")}`,
15088
15189
  {
@@ -15113,10 +15214,11 @@ ${notes.join("\n")}`,
15113
15214
  const next = [
15114
15215
  codeFile ? `Write the function in ${path9.relative(process.cwd(), codeFile)} and map its inputs under inputs.inputData in softr-workflows.jsonc.` : "",
15115
15216
  entry.type === "BRANCH" || entry.type === "FILTER" ? `Add the condition(s) on the path(s) leaving it in softr-workflows.jsonc (see "spec ${entry.type}").` : "",
15116
- `Then: softr-workflows push, softr-workflows test ${id}.`
15217
+ `Then: softr workflows push, softr workflows test ${id}.`
15117
15218
  ].filter(Boolean);
15118
- emit(
15219
+ emitWithVersionNote(
15119
15220
  options,
15221
+ config,
15120
15222
  `Added ${entry.type} "${id}" after "${after}"${opts.inLoop ? ` inside loop "${opts.inLoop}"` : ""}.
15121
15223
  ${next.join("\n")}`,
15122
15224
  {
@@ -15158,7 +15260,10 @@ async function resolveWorkspace(client, requested) {
15158
15260
  );
15159
15261
  }
15160
15262
  function registerInit(program3) {
15161
- program3.command("init").description("Create a new workflow with one trigger, on the server and as a local directory").argument("<title>", "the workflow title").option("--workspace <id>", "workspace to create it in (default: your only or default workspace)").option("--trigger <TYPE>", 'trigger type from "spec" (default WEBHOOK)', "WEBHOOK").option("--dir <path>", "directory to create (default: ./<title-slug>)").action(async (title, opts, command) => {
15263
+ program3.command("init").description("Create a new workflow with one trigger, on the server and as a local directory").argument("<title>", "the workflow title").option(
15264
+ "--workspace <id>",
15265
+ "workspace to create it in (default: $SOFTR_WORKSPACE_ID, else your only or default workspace)"
15266
+ ).option("--trigger <TYPE>", 'trigger type from "spec" (default WEBHOOK)', "WEBHOOK").option("--dir <path>", "directory to create (default: ./<title-slug>)").action(async (title, opts, command) => {
15162
15267
  const options = globalOptions(command);
15163
15268
  const cwd = process.cwd();
15164
15269
  const dir = path10.resolve(cwd, opts.dir ?? slug(title));
@@ -15168,7 +15273,7 @@ function registerInit(program3) {
15168
15273
  const trigger = findCatalogEntry(opts.trigger, "trigger");
15169
15274
  const target = resolveTarget();
15170
15275
  const client = await authenticatedClient(target);
15171
- const workspaceId = await resolveWorkspace(client, opts.workspace);
15276
+ const workspaceId = await resolveWorkspace(client, opts.workspace ?? process.env["SOFTR_WORKSPACE_ID"]);
15172
15277
  const created = await createWorkflow(client, {
15173
15278
  workspaceId,
15174
15279
  applicationId: null,
@@ -15198,7 +15303,7 @@ function registerInit(program3) {
15198
15303
  `Created "${title}" (${created.id}) with a ${trigger.type} trigger "${TRIGGER_ID}" in workspace ${workspaceId}.
15199
15304
  ${path10.relative(cwd, configFile)}
15200
15305
  ${studioUrl}
15201
- Next: softr-workflows add <TYPE> --dir ${path10.relative(cwd, dir) || "."} (see "spec" for the types), then push and test.`,
15306
+ Next: softr workflows add <TYPE> --dir ${path10.relative(cwd, dir) || "."} (see "spec" for the types), then push and test.`,
15202
15307
  {
15203
15308
  workflowId: created.id,
15204
15309
  workspaceId,
@@ -15263,12 +15368,15 @@ async function workspaceFor(client, requested, workflowDir) {
15263
15368
  return resolveWorkspace(client, void 0);
15264
15369
  }
15265
15370
  function registerIntegrations(program3) {
15266
- program3.command("integrations").description("List the workspace's connected integrations, whose ids go into integrationId inputs").option("--workspace <id>", "workspace to list (default: the workflow directory's, else your only or default one)").option("--type <TYPE>", "only this integration type, e.g. SLACK or SOFTR_TABLES").option("--dir <path>", "workflow directory whose workspace to use (default: the current directory)").action(async (opts, command) => {
15371
+ program3.command("integrations").description("List the workspace's connected integrations, whose ids go into integrationId inputs").option(
15372
+ "--workspace <id>",
15373
+ "workspace to list (default: $SOFTR_WORKSPACE_ID, else the workflow directory's, else your only or default workspace)"
15374
+ ).option("--type <TYPE>", "only this integration type, e.g. SLACK or SOFTR_TABLES").option("--dir <path>", "workflow directory whose workspace to use (default: the current directory)").action(async (opts, command) => {
15267
15375
  const options = globalOptions(command);
15268
15376
  const workflowDir = await workflowDirOf(process.cwd(), opts.dir);
15269
15377
  const target = resolveTarget(await readState(workflowDir ?? process.cwd()));
15270
15378
  const client = await authenticatedClient(target);
15271
- const workspaceId = await workspaceFor(client, opts.workspace, workflowDir);
15379
+ const workspaceId = await workspaceFor(client, opts.workspace ?? process.env["SOFTR_WORKSPACE_ID"], workflowDir);
15272
15380
  const wanted = opts.type?.toUpperCase();
15273
15381
  const rows = (await listWorkspaceIntegrations(client, workspaceId)).map(toRow2).filter((row) => !wanted || row.type === wanted).sort(byTypeThenName);
15274
15382
  emit(options, `Workspace ${workspaceId}${wanted ? `, type ${wanted}` : ""}:
@@ -15322,7 +15430,7 @@ async function interactiveLogin(target) {
15322
15430
  if (answer === "2") {
15323
15431
  const token = (await rl.question("Personal API token: ")).trim();
15324
15432
  if (!token) {
15325
- throw new CliError("No token given.", 'Run "softr-workflows login" again.');
15433
+ throw new CliError("No token given.", 'Run "softr login" again.');
15326
15434
  }
15327
15435
  return pastedToken(token);
15328
15436
  }
@@ -15350,7 +15458,7 @@ function waitForCallback(expectedState) {
15350
15458
  }
15351
15459
  const finish = (status, text) => {
15352
15460
  response.writeHead(status, { "Content-Type": "text/html; charset=utf-8", Connection: "close" });
15353
- response.end(`<!doctype html><title>softr-workflows</title><p>${text} You can close this tab.</p>`);
15461
+ response.end(`<!doctype html><title>softr</title><p>${text} You can close this tab.</p>`);
15354
15462
  server.close();
15355
15463
  };
15356
15464
  const error = url.searchParams.get("error");
@@ -15377,9 +15485,7 @@ function waitForCallback(expectedState) {
15377
15485
  }
15378
15486
  const timer = setTimeout(() => {
15379
15487
  server.close();
15380
- settle?.reject(
15381
- new CliError("No answer from the browser within 5 minutes.", 'Run "softr-workflows login" again.')
15382
- );
15488
+ settle?.reject(new CliError("No answer from the browser within 5 minutes.", 'Run "softr login" again.'));
15383
15489
  }, CALLBACK_TIMEOUT_MS);
15384
15490
  timer.unref();
15385
15491
  resolveServer({ redirectUri: `http://127.0.0.1:${address.port}/callback`, result });
@@ -15484,51 +15590,6 @@ function outdatedReferences(row) {
15484
15590
  const outdated = row?.sampleOutput?.outdatedDependencies;
15485
15591
  return outdated && Object.keys(outdated).length ? outdated : {};
15486
15592
  }
15487
- function assessTests(config, rows, lastSavedAt) {
15488
- const byId = new Map(rows.map((row) => [row.id, row]));
15489
- const issues = [];
15490
- for (const node of executionOrder(config)) {
15491
- if (!node.testable) {
15492
- continue;
15493
- }
15494
- const row = byId.get(node.id);
15495
- const result = row?.sampleOutput?.result;
15496
- if (!result) {
15497
- issues.push({ id: node.id, type: node.type, kind: "untested", detail: "not tested" });
15498
- } else if (result.status === "FAILED") {
15499
- issues.push({
15500
- id: node.id,
15501
- type: node.type,
15502
- kind: "failed",
15503
- detail: `last test failed${result.errorMessage ? `: ${result.errorMessage}` : ""}`
15504
- });
15505
- } else if (result.status !== "SUCCEEDED") {
15506
- issues.push({
15507
- id: node.id,
15508
- type: node.type,
15509
- kind: "pending",
15510
- detail: `last test ended with ${result.status ?? "no status"}`
15511
- });
15512
- } else if (lastSavedAt && row?.sampleOutput?.createdAt && new Date(row.sampleOutput.createdAt) < new Date(lastSavedAt)) {
15513
- issues.push({
15514
- id: node.id,
15515
- type: node.type,
15516
- kind: "stale",
15517
- detail: `tested ${row.sampleOutput.createdAt}, before the last save ${lastSavedAt}; test again to be sure`
15518
- });
15519
- }
15520
- const outdated = Object.entries(outdatedReferences(row));
15521
- if (outdated.length) {
15522
- issues.push({
15523
- id: node.id,
15524
- type: node.type,
15525
- kind: "outdated",
15526
- detail: `tested against outdated data of ${outdated.map(([upstream, paths]) => `${upstream} (${paths.join(", ")})`).join("; ")}`
15527
- });
15528
- }
15529
- }
15530
- return issues;
15531
- }
15532
15593
  function renderIssues(issues) {
15533
15594
  return issues.map((issue) => ` - ${issue.id} (${issue.type}): ${issue.detail}`).join("\n");
15534
15595
  }
@@ -15618,7 +15679,7 @@ async function runOne(client, workflowId, node, opts) {
15618
15679
  if (!node.testable) {
15619
15680
  return { ...base, outcome: "skipped", detail: "branches, filters and loops have no output of their own" };
15620
15681
  }
15621
- const row = node.kind === "trigger" ? await runTrigger(client, workflowId, node.id, opts.sample ?? 0) : await testAction(client, workflowId, node.id, opts.mock ? "mock" : "test", opts.iteration);
15682
+ const row = node.kind === "trigger" ? await runTrigger(client, workflowId, node.id, opts.sample ?? 0) : await testAction(client, workflowId, hierarchyPath(node, opts.iteration), opts.mock ? "mock" : "test");
15622
15683
  return { ...base, ...outcomeOf(row), row };
15623
15684
  }
15624
15685
  function mergeRows(existing, fresh) {
@@ -15688,7 +15749,7 @@ function registerTest(program3) {
15688
15749
  "Run a node in the workflows service and save its sample output; --all runs every node in execution order"
15689
15750
  ).argument("[nodeId]", "the node to test (its id in softr-workflows.jsonc)").option("--all", "test the trigger and then every action, predecessors first, skipping branches, filters and loops").option("--mock", "mock run instead of a real one (actions only; custom code always runs for real)").option(
15690
15751
  "--iteration <n>",
15691
- "for a node inside a loop: which iteration to use",
15752
+ "for a node inside a loop: the iteration of the enclosing loop(s) to use (default 0)",
15692
15753
  (value) => Number.parseInt(value, 10)
15693
15754
  ).option(
15694
15755
  "--sample <n>",
@@ -15744,7 +15805,7 @@ function registerTest(program3) {
15744
15805
  ].join("\n")
15745
15806
  );
15746
15807
  }
15747
- emit(options, [renderResults(results), notes.join("\n")].filter(Boolean).join("\n\n"), {
15808
+ emitWithVersionNote(options, config, [renderResults(results), notes.join("\n")].filter(Boolean).join("\n\n"), {
15748
15809
  workflowId: config.id,
15749
15810
  results: results.map(({ row: _row, ...result }) => result),
15750
15811
  headersRefreshed: headers.refreshed,
@@ -15831,7 +15892,7 @@ function registerOutputs(program3) {
15831
15892
  const row = byId.get(nodeId);
15832
15893
  const sample = row?.sampleOutput?.result?.output;
15833
15894
  if (!row || sample === void 0 || sample === null) {
15834
- throw new CliError(`No sample output for "${nodeId}" yet.`, `Run: softr-workflows test ${nodeId}`);
15895
+ throw new CliError(`No sample output for "${nodeId}" yet.`, `Run: softr workflows test ${nodeId}`);
15835
15896
  }
15836
15897
  const listing = collectVariables(nodeId, sample, row.outputMetadata ?? []);
15837
15898
  emit(options, renderVariables(nodeId, listing.variables, listing.unverified), {
@@ -15869,7 +15930,7 @@ function registerOutputs(program3) {
15869
15930
  refreshedHeaders.length ? `Headers refreshed: ${refreshedHeaders.join(", ")}` : "",
15870
15931
  problems.length ? `Code files that do not parse (headers not refreshed):
15871
15932
  ${problems.map((problem) => ` ${problem}`).join("\n")}` : "",
15872
- 'Run "softr-workflows outputs <nodeId>" for the variables of one node.'
15933
+ 'Run "softr workflows outputs <nodeId>" for the variables of one node.'
15873
15934
  ].filter(Boolean);
15874
15935
  emit(options, `${table.toString()}
15875
15936
 
@@ -15957,7 +16018,7 @@ function registerPlaceholder(program3) {
15957
16018
  }
15958
16019
  throw new CliError(
15959
16020
  `${token} is not backed by test data: ${hint}.`,
15960
- `Run "softr-workflows outputs ${nodeId}" to list every referenceable path.`
16021
+ `Run "softr workflows outputs ${nodeId}" to list every referenceable path.`
15961
16022
  );
15962
16023
  }
15963
16024
  const type = renderJsType(description.type);
@@ -15979,55 +16040,6 @@ type: ${type}${example3} (${status})`, {
15979
16040
  import { stdin as stdin2, stdout } from "node:process";
15980
16041
  import { createInterface as createInterface2 } from "node:readline/promises";
15981
16042
 
15982
- // src/project/placeholder-check.ts
15983
- var TOKEN = /\{(?:outputs|loopActionGroup)\.[^:{}]+:::[^{}]*\}/g;
15984
- function* stringsIn(value, path13) {
15985
- if (typeof value === "string") {
15986
- yield [path13, value];
15987
- } else if (Array.isArray(value)) {
15988
- for (const [index, item] of value.entries()) {
15989
- yield* stringsIn(item, `${path13}[${index}]`);
15990
- }
15991
- } else if (value !== null && typeof value === "object") {
15992
- for (const [key, item] of Object.entries(value)) {
15993
- if (key === "actions" || key === "paths") {
15994
- continue;
15995
- }
15996
- yield* stringsIn(item, path13 ? `${path13}.${key}` : key);
15997
- }
15998
- }
15999
- }
16000
- function checkPlaceholders(config, outputs) {
16001
- const findings = [];
16002
- const nodes = [...config.triggers ?? [], ...allActions(config.actions)];
16003
- for (const node of nodes) {
16004
- for (const [input, text] of stringsIn(node.inputs ?? {}, "")) {
16005
- for (const token of new Set(text.match(TOKEN) ?? [])) {
16006
- if (!parseWholePlaceholder(token)) {
16007
- continue;
16008
- }
16009
- const [description] = describeInputData(
16010
- { id: node.id, type: node.type, inputs: { inputData: { probe: token } } },
16011
- config,
16012
- outputs
16013
- );
16014
- if (!description) {
16015
- continue;
16016
- }
16017
- const status = description.verified ? "verified" : description.type.kind === "unknown" ? "unknown" : "declared";
16018
- findings.push({
16019
- nodeId: node.id,
16020
- input,
16021
- token,
16022
- status,
16023
- ...description.note ? { note: description.note } : {}
16024
- });
16025
- }
16026
- }
16027
- }
16028
- return findings;
16029
- }
16030
-
16031
16043
  // src/project/readiness.ts
16032
16044
  var REASONS = {
16033
16045
  REFERENCED_NODE_UNKNOWN: "no node with that id",
@@ -16074,31 +16086,15 @@ function fromReadiness(readiness) {
16074
16086
  }
16075
16087
  }
16076
16088
  return {
16077
- source: "service",
16078
16089
  validationProblems: readiness.validationProblems,
16079
16090
  testIssues,
16080
16091
  unbacked,
16081
16092
  checked: { version: readiness.version, draft: readiness.draft }
16082
16093
  };
16083
16094
  }
16084
- function fromClientChecks(config, rows, lastUpdatedAt) {
16085
- const outputs = indexOutputs(rows);
16086
- return {
16087
- source: "client",
16088
- validationProblems: null,
16089
- testIssues: assessTests(config, rows, lastUpdatedAt),
16090
- unbacked: checkPlaceholders(config, outputs).filter((finding) => finding.status !== "verified").map((finding) => ({ nodeId: finding.nodeId, token: finding.token, reason: finding.note ?? finding.status })),
16091
- checked: null
16092
- };
16093
- }
16094
- async function assess(client, workflowId, packed, rows, lastUpdatedAt) {
16095
- const readiness = await getWorkflowReadiness(client, workflowId);
16096
- if (readiness) {
16097
- return fromReadiness(readiness);
16098
- }
16099
- return fromClientChecks(packed ?? { actions: [], triggers: [] }, rows, lastUpdatedAt);
16095
+ async function assess(client, workflowId) {
16096
+ return fromReadiness(await getWorkflowReadiness(client, workflowId));
16100
16097
  }
16101
- var CLIENT_SIDE_NOTE = "Checked by this CLI: this deployment of the service has no readiness endpoint (it answered the request the way it answers an unknown route), so the publish validation is not part of this report.";
16102
16098
 
16103
16099
  // src/commands/publish.ts
16104
16100
  async function openWorkflow(dir) {
@@ -16134,15 +16130,15 @@ function registerPublish(program3) {
16134
16130
  if (state?.syncedHash && definitionHash(packed) !== state.syncedHash) {
16135
16131
  throw new CliError("Local edits are not pushed yet.", "Run push first, then publish.");
16136
16132
  }
16137
- const [remote, rows] = await Promise.all([getWorkflow(client, id), getWorkflowOutputs(client, id)]);
16133
+ const remote = await getWorkflow(client, id);
16138
16134
  if (state && (remote.version !== state.version || remote.draft !== state.draft) && !opts.force) {
16139
16135
  throw new CliError(
16140
16136
  `The server copy changed since your last sync: it is version ${remote.version}, draft ${remote.draft}; this directory knows version ${state.version}, draft ${state.draft}.`,
16141
16137
  'Run "pull --force" to see what would go live, then publish; or "publish --force" to enable it unseen.'
16142
16138
  );
16143
16139
  }
16144
- const assessment = await assess(client, id, packed, rows, remote.lastUpdatedAt);
16145
- if (assessment.validationProblems?.length) {
16140
+ const assessment = await assess(client, id);
16141
+ if (assessment.validationProblems.length) {
16146
16142
  throw new CliError(
16147
16143
  `The service would reject publishing:
16148
16144
  ${assessment.validationProblems.map((problem) => ` - ${problem}`).join("\n")}`,
@@ -16152,8 +16148,7 @@ ${assessment.validationProblems.map((problem) => ` - ${problem}`).join("\n")}`,
16152
16148
  const issues = issuesOf(assessment, config);
16153
16149
  if (issues.length && !opts.force) {
16154
16150
  const text = `The workflow is not fully tested:
16155
- ${renderIssues(issues)}${assessment.source === "client" ? `
16156
- ${CLIENT_SIDE_NOTE}` : ""}`;
16151
+ ${renderIssues(issues)}`;
16157
16152
  if (stdin2.isTTY && stdout.isTTY) {
16158
16153
  const rl = createInterface2({ input: stdin2, output: stdout });
16159
16154
  let answer;
@@ -16168,7 +16163,7 @@ Publish anyway? [y/N] `);
16168
16163
  return;
16169
16164
  }
16170
16165
  } else {
16171
- throw new CliError(text, 'Run "softr-workflows test --all" first, or pass --force to publish anyway.');
16166
+ throw new CliError(text, 'Run "softr workflows test --all" first, or pass --force to publish anyway.');
16172
16167
  }
16173
16168
  }
16174
16169
  await enableWorkflow(client, id);
@@ -16190,20 +16185,8 @@ ${target.studioUrl}/workflow/${id}`,
16190
16185
  const options = globalOptions(command);
16191
16186
  const dir = await findWorkflowDir(process.cwd(), opts.dir);
16192
16187
  const { id, config, client, target, state } = await openWorkflow(dir);
16193
- const [remote, live, rows] = await Promise.all([
16194
- getWorkflow(client, id),
16195
- getEnabledVersion(client, id),
16196
- getWorkflowOutputs(client, id)
16197
- ]);
16198
- let packedForStatus;
16199
- try {
16200
- packedForStatus = (await packWorkflow(config, dir)).packed;
16201
- } catch (error) {
16202
- if (!(error instanceof CliError)) {
16203
- throw error;
16204
- }
16205
- }
16206
- const assessment = await assess(client, id, packedForStatus, rows, remote.lastUpdatedAt);
16188
+ const [remote, live] = await Promise.all([getWorkflow(client, id), getEnabledVersion(client, id)]);
16189
+ const assessment = await assess(client, id);
16207
16190
  const issues = issuesOf(assessment, config);
16208
16191
  let local = "unknown";
16209
16192
  let problem;
@@ -16224,15 +16207,12 @@ ${target.studioUrl}/workflow/${id}`,
16224
16207
  `Local: ${local}${problem ? ` (${problem})` : ""}${state ? `; last sync version ${state.version}, draft ${state.draft}` : ""}`,
16225
16208
  `Server: version ${remote.version}, draft ${remote.draft}${serverMoved ? " (moved since your last sync: pull --force to take it)" : ""}`,
16226
16209
  `Live: ${live ? `version ${live.version}` : "not published"}`,
16227
- ...assessment.validationProblems === null ? [] : [
16228
- `Valid: ${assessment.validationProblems.length ? `${assessment.validationProblems.length} problem(s)
16229
- ${assessment.validationProblems.map((problem2) => ` - ${problem2}`).join("\n")}` : "the service accepts the workflow for publishing"}`
16230
- ],
16210
+ `Valid: ${assessment.validationProblems.length ? `${assessment.validationProblems.length} problem(s)
16211
+ ${assessment.validationProblems.map((problem2) => ` - ${problem2}`).join("\n")}` : "the service accepts the workflow for publishing"}`,
16231
16212
  `Tests: ${issues.length ? `${issues.length} issue(s)
16232
- ${renderIssues(issues)}` : "every node tested on current data"}`,
16233
- ...assessment.source === "client" ? [CLIENT_SIDE_NOTE] : []
16213
+ ${renderIssues(issues)}` : "every node tested on current data"}`
16234
16214
  ];
16235
- emit(options, lines.join("\n"), {
16215
+ emitWithVersionNote(options, config, lines.join("\n"), {
16236
16216
  workflowId: id,
16237
16217
  title: remote.title,
16238
16218
  studioUrl: `${target.studioUrl}/workflow/${id}`,
@@ -16242,14 +16222,64 @@ ${renderIssues(issues)}` : "every node tested on current data"}`,
16242
16222
  server: { version: remote.version, draft: remote.draft, moved: serverMoved },
16243
16223
  liveVersion: live?.version ?? null,
16244
16224
  testIssues: issues,
16245
- validationProblems: assessment.validationProblems,
16246
- source: assessment.source
16225
+ validationProblems: assessment.validationProblems
16247
16226
  });
16248
16227
  });
16249
16228
  }
16250
16229
 
16251
16230
  // src/commands/push.ts
16252
16231
  import path12 from "node:path";
16232
+
16233
+ // src/project/placeholder-check.ts
16234
+ var TOKEN = /\{(?:outputs|loopActionGroup)\.[^:{}]+:::[^{}]*\}/g;
16235
+ function* stringsIn(value, path13) {
16236
+ if (typeof value === "string") {
16237
+ yield [path13, value];
16238
+ } else if (Array.isArray(value)) {
16239
+ for (const [index, item] of value.entries()) {
16240
+ yield* stringsIn(item, `${path13}[${index}]`);
16241
+ }
16242
+ } else if (value !== null && typeof value === "object") {
16243
+ for (const [key, item] of Object.entries(value)) {
16244
+ if (key === "actions" || key === "paths") {
16245
+ continue;
16246
+ }
16247
+ yield* stringsIn(item, path13 ? `${path13}.${key}` : key);
16248
+ }
16249
+ }
16250
+ }
16251
+ function checkPlaceholders(config, outputs) {
16252
+ const findings = [];
16253
+ const nodes = [...config.triggers ?? [], ...allActions(config.actions)];
16254
+ for (const node of nodes) {
16255
+ for (const [input, text] of stringsIn(node.inputs ?? {}, "")) {
16256
+ for (const token of new Set(text.match(TOKEN) ?? [])) {
16257
+ if (!parseWholePlaceholder(token)) {
16258
+ continue;
16259
+ }
16260
+ const [description] = describeInputData(
16261
+ { id: node.id, type: node.type, inputs: { inputData: { probe: token } } },
16262
+ config,
16263
+ outputs
16264
+ );
16265
+ if (!description) {
16266
+ continue;
16267
+ }
16268
+ const status = description.verified ? "verified" : description.type.kind === "unknown" ? "unknown" : "declared";
16269
+ findings.push({
16270
+ nodeId: node.id,
16271
+ input,
16272
+ token,
16273
+ status,
16274
+ ...description.note ? { note: description.note } : {}
16275
+ });
16276
+ }
16277
+ }
16278
+ }
16279
+ return findings;
16280
+ }
16281
+
16282
+ // src/commands/push.ts
16253
16283
  function registerPush(program3) {
16254
16284
  program3.command("push").description("Inline @@ file references and code files, then save the workflow as a new draft").option("--dir <path>", "workflow directory (default: the current directory)").option("--dry-run", "print the request body instead of sending it").option("--force", "push even when nothing changed locally or the server copy changed since the last sync").action(async (opts, command) => {
16255
16285
  const options = globalOptions(command);
@@ -16308,8 +16338,9 @@ function registerPush(program3) {
16308
16338
  syncedHash: hash
16309
16339
  });
16310
16340
  const studioUrl = `${target.studioUrl}/workflow/${config.id}`;
16311
- emit(
16341
+ emitWithVersionNote(
16312
16342
  options,
16343
+ config,
16313
16344
  `Pushed "${packed.title}": version ${result.version}, draft ${result.draft}` + (inlined.length ? ` (${inlined.length} file${inlined.length === 1 ? "" : "s"} inlined)` : "") + `.
16314
16345
  ${studioUrl}` + (unbacked.length ? `
16315
16346
  Warning: ${unbacked.length} placeholder(s) not backed by test data:
@@ -16347,7 +16378,7 @@ function renderList() {
16347
16378
  return [
16348
16379
  table.toString(),
16349
16380
  "This CLI offers only these node types on purpose: code-first workflows use custom code for everything else.",
16350
- 'Run "softr-workflows spec <TYPE>" for the inputs, output and an example of one type.'
16381
+ 'Run "softr workflows spec <TYPE>" for the inputs, output and an example of one type.'
16351
16382
  ].join("\n");
16352
16383
  }
16353
16384
  function integrationIdFields(entry) {
@@ -16382,7 +16413,7 @@ ${table.toString()}`);
16382
16413
  const idFields = integrationIdFields(entry);
16383
16414
  if (idFields.length) {
16384
16415
  blocks.push(
16385
- `${idFields.join(", ")}: run "softr-workflows integrations" for the ids of the workspace's connected integrations.`
16416
+ `${idFields.join(", ")}: run "softr integrations" for the ids of the workspace's connected integrations.`
16386
16417
  );
16387
16418
  }
16388
16419
  } else {
@@ -16424,6 +16455,75 @@ function registerSpec(program3) {
16424
16455
  });
16425
16456
  }
16426
16457
 
16458
+ // src/commands/upgrade.ts
16459
+ function registerUpgrade(program3) {
16460
+ program3.command("upgrade").description(
16461
+ "Move nodes on an older version of their type to the current one: CUSTOM_CODE 1.0.0 and 1.1.0 become 1.2.0 (the integrations list is added, the code file keeps its body and gets the new header)"
16462
+ ).argument("[nodeIds...]", "the nodes to upgrade (default: every node the CLI can upgrade)").option("--dir <path>", "workflow directory (default: the current directory)").action(async (nodeIds, opts, command) => {
16463
+ const options = globalOptions(command);
16464
+ const dir = await findWorkflowDir(process.cwd(), opts.dir);
16465
+ const config = await readConfig(dir);
16466
+ const outdated = outdatedNodes(config);
16467
+ for (const id of nodeIds) {
16468
+ if (!locateNode(config, id)) {
16469
+ throw new CliError(
16470
+ `No node "${id}" in this workflow.`,
16471
+ 'Node ids are the "id" values in softr-workflows.jsonc.'
16472
+ );
16473
+ }
16474
+ }
16475
+ const wanted = nodeIds.length ? outdated.filter((node) => nodeIds.includes(node.id)) : outdated;
16476
+ const current2 = nodeIds.filter((id) => !outdated.some((node) => node.id === id));
16477
+ const upgraded = [];
16478
+ const manual = [];
16479
+ const edits = [];
16480
+ for (const node of wanted) {
16481
+ const location = locateNode(config, node.id);
16482
+ const next = location ? upgradeOf(location.node) : void 0;
16483
+ if (!location || !next) {
16484
+ manual.push(node);
16485
+ continue;
16486
+ }
16487
+ edits.push({ path: location.path, value: next });
16488
+ upgraded.push(node);
16489
+ }
16490
+ if (edits.length) {
16491
+ await editConfig(dir, edits);
16492
+ }
16493
+ const headers = edits.length ? await refreshCodeHeaders(
16494
+ await readConfig(dir),
16495
+ dir,
16496
+ indexOutputs(await readStateFile(dir, OUTPUTS_FILE) ?? [])
16497
+ ) : { refreshed: [], problems: [] };
16498
+ if (!wanted.length && manual.length === 0 && current2.length === 0) {
16499
+ emit(options, "Every node is on the current version of its type.", { upgraded: [], manual: [], current: [] });
16500
+ return;
16501
+ }
16502
+ if (headers.problems.length) {
16503
+ throw new CliError(
16504
+ `Upgraded, but a code file could not get its new header:
16505
+ ${headers.problems.map((problem) => ` ${problem}`).join("\n")}`,
16506
+ 'Fix the file, then run "pull" to regenerate its header.'
16507
+ );
16508
+ }
16509
+ const lines = [
16510
+ ...upgraded.map((node) => `Upgraded ${node.id}: ${node.type} ${node.version} \u2192 ${node.current}.`),
16511
+ ...manual.map(
16512
+ (node) => `No automatic upgrade for ${node.id}: ${node.type} ${node.version} \u2192 ${node.current}; compare its inputs with "spec ${node.type}" and set the version by hand.`
16513
+ ),
16514
+ ...current2.map((id) => `${id} is already on the current version.`),
16515
+ headers.refreshed.length ? `Headers refreshed: ${headers.refreshed.join(", ")}` : "",
16516
+ upgraded.length ? "Then: softr workflows push, softr workflows test --all." : ""
16517
+ ].filter(Boolean);
16518
+ emit(options, lines.join("\n"), {
16519
+ upgraded: upgraded.map((node) => ({ id: node.id, type: node.type, from: node.version, to: node.current })),
16520
+ manual: manual.map((node) => ({ id: node.id, type: node.type, from: node.version, to: node.current })),
16521
+ current: current2,
16522
+ headersRefreshed: headers.refreshed
16523
+ });
16524
+ });
16525
+ }
16526
+
16427
16527
  // src/commands/verify.ts
16428
16528
  function section(title, lines, okText) {
16429
16529
  return { title, ok: lines.length === 0, lines: lines.length ? lines : [okText] };
@@ -16479,7 +16579,7 @@ function registerVerify(program3) {
16479
16579
  `in sync with the server (version ${remote.version}, draft ${remote.draft})`
16480
16580
  );
16481
16581
  let rows = await getWorkflowOutputs(client, config.id);
16482
- let assessment = await assess(client, config.id, packed, rows, remote.lastUpdatedAt);
16582
+ let assessment = await assess(client, config.id);
16483
16583
  let ran = [];
16484
16584
  if (opts.testMissing && sync.ok && files.ok) {
16485
16585
  const wanted = new Set(
@@ -16489,23 +16589,19 @@ function registerVerify(program3) {
16489
16589
  if (nodes.length) {
16490
16590
  ran = await runNodeTests(client, config.id, nodes, {}, true);
16491
16591
  rows = await getWorkflowOutputs(client, config.id);
16492
- assessment = await assess(client, config.id, packed, rows, remote.lastUpdatedAt);
16592
+ assessment = await assess(client, config.id);
16493
16593
  }
16494
16594
  }
16495
16595
  await writeStateFile(dir, OUTPUTS_FILE, mergeRows([], rows));
16496
16596
  const headers = await refreshCodeHeaders(config, dir, indexOutputs(rows));
16497
- if (assessment.checked && (assessment.checked.version !== remote.version || assessment.checked.draft !== remote.draft)) {
16597
+ if (assessment.checked.version !== remote.version || assessment.checked.draft !== remote.draft) {
16498
16598
  syncLines.push(
16499
16599
  `the service assessed version ${assessment.checked.version}, draft ${assessment.checked.draft}, but the workflow is already at version ${remote.version}, draft ${remote.draft} (run verify again)`
16500
16600
  );
16501
16601
  }
16502
16602
  const sections = [files, sync];
16503
- if (assessment.validationProblems !== null) {
16504
- sections.push(
16505
- section("Validation", assessment.validationProblems, "the service accepts the workflow for publishing")
16506
- );
16507
- }
16508
16603
  sections.push(
16604
+ section("Validation", assessment.validationProblems, "the service accepts the workflow for publishing"),
16509
16605
  section(
16510
16606
  "Tests",
16511
16607
  assessment.testIssues.map((issue) => `${issue.id} (${issue.type}): ${issue.detail}`),
@@ -16525,17 +16621,15 @@ function registerVerify(program3) {
16525
16621
  (item) => `${item.ok ? "OK " : "NOT"} ${item.title}
16526
16622
  ${item.lines.map((line) => ` ${line}`).join("\n")}`
16527
16623
  ),
16528
- assessment.source === "client" ? CLIENT_SIDE_NOTE : "",
16529
16624
  headers.refreshed.length ? `Headers refreshed: ${headers.refreshed.join(", ")}` : "",
16530
16625
  ready ? "Ready to publish." : `Not ready to publish: ${sections.filter((item) => !item.ok).length} section(s) need attention.`
16531
16626
  ].filter(Boolean).join("\n\n");
16532
- emit(options, human, {
16627
+ emitWithVersionNote(options, config, human, {
16533
16628
  workflowId: config.id,
16534
16629
  ready,
16535
- source: assessment.source,
16536
16630
  files: { ok: files.ok, problems: fileProblems },
16537
16631
  sync: { ok: sync.ok, problems: syncLines, server: { version: remote.version, draft: remote.draft } },
16538
- validation: assessment.validationProblems === null ? null : { ok: assessment.validationProblems.length === 0, problems: assessment.validationProblems },
16632
+ validation: { ok: assessment.validationProblems.length === 0, problems: assessment.validationProblems },
16539
16633
  tests: {
16540
16634
  ok: assessment.testIssues.length === 0,
16541
16635
  issues: assessment.testIssues,
@@ -16549,10 +16643,36 @@ ${item.lines.map((line) => ` ${line}`).join("\n")}`
16549
16643
  });
16550
16644
  }
16551
16645
 
16646
+ // src/commands/workspaces.ts
16647
+ var import_cli_table36 = __toESM(require_cli_table3(), 1);
16648
+ function registerWorkspaces(program3) {
16649
+ program3.command("workspaces").description("The workspaces the stored token reaches, with your role in each").action(async (_opts, command) => {
16650
+ const options = globalOptions(command);
16651
+ const target = resolveTarget();
16652
+ const client = await authenticatedClient(target);
16653
+ const workspaces = await listWorkspaces(client);
16654
+ const table = new import_cli_table36.default({
16655
+ head: ["ID", "Name", "Role", "Default"],
16656
+ colWidths: [38, 34, 14, 9],
16657
+ wordWrap: true,
16658
+ style: { head: [], border: [], "padding-left": 1, "padding-right": 1 }
16659
+ });
16660
+ for (const workspace of workspaces) {
16661
+ table.push([workspace.id, workspace.name, workspace.userRole ?? "", workspace.isDefault ? "yes" : ""]);
16662
+ }
16663
+ emit(
16664
+ options,
16665
+ workspaces.length ? `${table.toString()}
16666
+ Pass --workspace <id> to a command, or set SOFTR_WORKSPACE_ID, to work in one of them.` : "The token reaches no workspace.",
16667
+ { workspaces }
16668
+ );
16669
+ });
16670
+ }
16671
+
16552
16672
  // package.json
16553
16673
  var package_default = {
16554
16674
  name: "@softr/cli",
16555
- version: "0.0.1",
16675
+ version: "0.1.0",
16556
16676
  description: "The Softr command line: build, test and publish Softr workflows from local files; more Softr APIs to come",
16557
16677
  keywords: [
16558
16678
  "softr",
@@ -16562,8 +16682,7 @@ var package_default = {
16562
16682
  license: "UNLICENSED",
16563
16683
  type: "module",
16564
16684
  bin: {
16565
- softr: "dist/cli.mjs",
16566
- "softr-workflows": "dist/cli.mjs"
16685
+ softr: "dist/cli.mjs"
16567
16686
  },
16568
16687
  files: [
16569
16688
  "dist"
@@ -16580,7 +16699,7 @@ var package_default = {
16580
16699
  format: "prettier --write .",
16581
16700
  "format:check": "prettier --check .",
16582
16701
  check: "npm run typecheck && npm run format:check && npm test && npm run build",
16583
- "check:skill": "npx -y skills-ref@0.1.5 validate skills/softr-workflows"
16702
+ "check:skill": "npx -y skills-ref@0.1.5 validate skills/softr-cli && npx -y skills-ref@0.1.5 validate skills/softr-workflows"
16584
16703
  },
16585
16704
  publishConfig: {
16586
16705
  access: "public"
@@ -16603,20 +16722,23 @@ var package_default = {
16603
16722
  };
16604
16723
 
16605
16724
  // src/cli.ts
16606
- var program2 = new Command().name("softr").description("Build, test and publish Softr Workflows from local files").version(package_default.version).option("--json", "print results as JSON");
16725
+ var program2 = new Command().name("softr").description("The Softr command line: workflows today, more of Softr to come").version(package_default.version).option("--json", "print results as JSON");
16607
16726
  registerLogin(program2);
16608
- registerList(program2);
16727
+ registerWorkspaces(program2);
16609
16728
  registerIntegrations(program2);
16610
- registerPull(program2);
16611
- registerPush(program2);
16612
- registerTest(program2);
16613
- registerVerify(program2);
16614
- registerOutputs(program2);
16615
- registerPlaceholder(program2);
16616
- registerInit(program2);
16617
- registerAdd(program2);
16618
- registerEdit(program2);
16619
- registerPublish(program2);
16620
- registerSpec(program2);
16621
- registerSchema(program2);
16729
+ var workflows = program2.command("workflows").description("Build, test and publish Softr Workflows from local files (one directory per workflow)");
16730
+ registerList(workflows);
16731
+ registerPull(workflows);
16732
+ registerPush(workflows);
16733
+ registerTest(workflows);
16734
+ registerVerify(workflows);
16735
+ registerOutputs(workflows);
16736
+ registerPlaceholder(workflows);
16737
+ registerInit(workflows);
16738
+ registerAdd(workflows);
16739
+ registerEdit(workflows);
16740
+ registerUpgrade(workflows);
16741
+ registerPublish(workflows);
16742
+ registerSpec(workflows);
16743
+ registerSchema(workflows);
16622
16744
  program2.parseAsync(process.argv).catch(reportError);