@softr/cli 0.0.1 → 0.1.1-se21216.10

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 +457 -312
  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",
@@ -6086,9 +6098,9 @@ var SoftrApiError = class extends Error {
6086
6098
  body;
6087
6099
  hint;
6088
6100
  };
6089
- function hintFor(status) {
6101
+ function hintFor(status, client) {
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 client.kind === "proxy" ? "The target refused the request: no access to this workflow, or its credential is gone." : "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.";
@@ -6096,7 +6108,7 @@ function hintFor(status) {
6096
6108
  return void 0;
6097
6109
  }
6098
6110
  function authHeaders(client) {
6099
- return { Authorization: `Bearer ${client.token}` };
6111
+ return client.kind === "proxy" ? {} : { Authorization: `Bearer ${client.token}` };
6100
6112
  }
6101
6113
  async function send(client, service, method, path13, body) {
6102
6114
  const init = {
@@ -6117,7 +6129,7 @@ async function send(client, service, method, path13, body) {
6117
6129
  `${method} ${path13} failed with status ${response.status}${text ? `: ${text}` : ""}`,
6118
6130
  response.status,
6119
6131
  text,
6120
- hintFor(response.status)
6132
+ hintFor(response.status, client)
6121
6133
  );
6122
6134
  }
6123
6135
  async function apiRequest(client, method, path13, body) {
@@ -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
  {
@@ -14287,13 +14397,12 @@ function updateWorkflow(client, workflowId, body) {
14287
14397
  function getWorkflowOutputs(client, workflowId) {
14288
14398
  return apiRequest(client, "GET", `/v1/workflows/${workflowId}/outputs`);
14289
14399
  }
14290
- function listWorkflows(client, workspaceId) {
14291
- const query = new URLSearchParams({ workspaceId, limit: "500" });
14400
+ function listWorkflows(client, workspaceId, applicationId) {
14401
+ const query = new URLSearchParams({ workspaceId, ...applicationId ? { applicationId } : {}, 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";
@@ -14415,7 +14514,7 @@ function normalizeUrl(value, source) {
14415
14514
  } catch {
14416
14515
  throw new CliError(`${source} is not a valid URL: ${value}`);
14417
14516
  }
14418
- return url.origin;
14517
+ return `${url.origin}${url.pathname.replace(/\/+$/, "")}`;
14419
14518
  }
14420
14519
  function resolveTarget(remembered) {
14421
14520
  const pick = (envVar, rememberedValue, fallback) => {
@@ -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,14 @@ 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";
14550
+ var AUTH_ENV_VAR = "SOFTR_AUTH";
14551
+ var PROXY_AUTH = "proxy";
14451
14552
  var REFRESH_MARGIN_MS = 5 * 60 * 1e3;
14452
14553
  function credentialsPath() {
14453
14554
  return path6.join(homedir(), ".softr", "credentials.json");
@@ -14512,13 +14613,16 @@ async function refreshedAccessToken(target, stored) {
14512
14613
  }
14513
14614
  }
14514
14615
  async function authenticatedClient(target) {
14616
+ if (process.env[AUTH_ENV_VAR] === PROXY_AUTH) {
14617
+ return { target, kind: "proxy" };
14618
+ }
14515
14619
  const fromEnv = process.env[TOKEN_ENV_VAR];
14516
14620
  if (fromEnv) {
14517
14621
  return { target, token: fromEnv, kind: tokenKind(fromEnv) };
14518
14622
  }
14519
14623
  const stored = await storedToken(target);
14520
14624
  if (!stored) {
14521
- throw new CliError(`No token stored for ${targetHost(target)}.`, "Run: softr-workflows login");
14625
+ throw new CliError(`No token stored for ${targetHost(target)}.`, "Run: softr login");
14522
14626
  }
14523
14627
  const kind = stored.kind ?? tokenKind(stored.token);
14524
14628
  const expiresAt = stored.expiresAt ? new Date(stored.expiresAt) : void 0;
@@ -14528,7 +14632,7 @@ async function authenticatedClient(target) {
14528
14632
  if (expiresAt && expiresAt < /* @__PURE__ */ new Date()) {
14529
14633
  throw new CliError(
14530
14634
  `The stored token for ${targetHost(target)} expired at ${stored.expiresAt}.`,
14531
- "Run: softr-workflows login"
14635
+ "Run: softr login"
14532
14636
  );
14533
14637
  }
14534
14638
  return { target, token: stored.token, kind };
@@ -14545,7 +14649,7 @@ async function resolveTokenOwner(client) {
14545
14649
  if (error instanceof SoftrApiError && error.status === 404) {
14546
14650
  throw new CliError(
14547
14651
  "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.'
14652
+ 'Create a new personal API token in the studio, or run "softr login" for the browser login.'
14549
14653
  );
14550
14654
  }
14551
14655
  throw error;
@@ -14621,7 +14725,7 @@ function toRow(workflow, workspace) {
14621
14725
  updatedAt: workflow.updatedAt ?? null
14622
14726
  };
14623
14727
  }
14624
- async function collectWorkflows(client, workspaceId) {
14728
+ async function collectWorkflows(client, workspaceId, applicationId) {
14625
14729
  const workspaces = await listWorkspaces(client);
14626
14730
  const selected = workspaceId ? workspaces.filter((workspace) => workspace.id === workspaceId) : workspaces;
14627
14731
  if (workspaceId && !selected.length) {
@@ -14633,7 +14737,7 @@ async function collectWorkflows(client, workspaceId) {
14633
14737
  const listing = { workspaces: [], workflows: [], failures: [] };
14634
14738
  for (const workspace of selected) {
14635
14739
  try {
14636
- const page = await listWorkflows(client, workspace.id);
14740
+ const page = await listWorkflows(client, workspace.id, applicationId);
14637
14741
  listing.workspaces.push({ id: workspace.id, name: workspace.name });
14638
14742
  listing.workflows.push(...page.items.map((workflow) => toRow(workflow, workspace)));
14639
14743
  } catch (error) {
@@ -14663,10 +14767,10 @@ function workflowTable(rows) {
14663
14767
  }
14664
14768
  return table.toString();
14665
14769
  }
14666
- function renderWorkflows({ workspaces, workflows, failures }) {
14770
+ function renderWorkflows({ workspaces, workflows: workflows2, failures }) {
14667
14771
  const blocks = [];
14668
14772
  for (const workspace of workspaces) {
14669
- const rows = workflows.filter((row) => row.workspaceId === workspace.id);
14773
+ const rows = workflows2.filter((row) => row.workspaceId === workspace.id);
14670
14774
  const heading = `Workspace: ${workspace.name} (${workspace.id})`;
14671
14775
  blocks.push(rows.length ? `${heading}
14672
14776
  ${workflowTable(rows)}` : `${heading}
@@ -14684,16 +14788,16 @@ ${workflowTable(rows)}` : `${heading}
14684
14788
  }
14685
14789
  return blocks.join("\n\n");
14686
14790
  }
14687
- async function emitWorkflowList(options, client, workspaceId, human) {
14688
- const listing = await collectWorkflows(client, workspaceId);
14791
+ async function emitWorkflowList(options, client, workspaceId, human, applicationId) {
14792
+ const listing = await collectWorkflows(client, workspaceId, applicationId);
14689
14793
  emit(options, [human, renderWorkflows(listing)].filter(Boolean).join("\n\n"), { ...listing });
14690
14794
  }
14691
14795
  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) => {
14796
+ 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)").option("--app <id>", "only the workflows linked to this application").action(async (opts, command) => {
14693
14797
  const options = globalOptions(command);
14694
14798
  const target = resolveTarget(await readState(process.cwd()));
14695
14799
  const client = await authenticatedClient(target);
14696
- await emitWorkflowList(options, client, opts.workspace);
14800
+ await emitWorkflowList(options, client, opts.workspace ?? process.env["SOFTR_WORKSPACE_ID"], void 0, opts.app);
14697
14801
  });
14698
14802
  }
14699
14803
 
@@ -14960,7 +15064,7 @@ function registerPull(program3) {
14960
15064
  options,
14961
15065
  client,
14962
15066
  void 0,
14963
- "Pick a workflow and run: softr-workflows pull <workflowId>"
15067
+ "Pick a workflow and run: softr workflows pull <workflowId>"
14964
15068
  );
14965
15069
  return;
14966
15070
  }
@@ -14991,8 +15095,9 @@ Sample outputs not available, inputData types are unknown: ${error.message}`;
14991
15095
  outputsFile = await writeStateFile(dir, "outputs.json", rows);
14992
15096
  }
14993
15097
  const files = [configFile, ...codeFiles].map((file) => path8.relative(cwd, file));
14994
- emit(
15098
+ emitWithVersionNote(
14995
15099
  options,
15100
+ config,
14996
15101
  `Pulled "${workflow.title}" (version ${workflow.version}, draft ${workflow.draft}, ${workflow.enabled ? "enabled" : "not enabled"}).
14997
15102
  ` + files.map((file) => ` ${file}`).join("\n") + outputsNote,
14998
15103
  {
@@ -15079,10 +15184,11 @@ function registerAdd(program3) {
15079
15184
  const notes = [
15080
15185
  (config.triggers?.length ?? 0) >= 1 ? "The service accepts one trigger per published workflow for now; multi-trigger support is coming." : "",
15081
15186
  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}.`
15187
+ `Then: softr workflows push, softr workflows test ${id}.`
15083
15188
  ].filter(Boolean);
15084
- emit(
15189
+ emitWithVersionNote(
15085
15190
  options,
15191
+ config,
15086
15192
  `Added ${entry.type} trigger "${id}"${opts.to ? ` leading to "${opts.to}"` : ""}.
15087
15193
  ${notes.join("\n")}`,
15088
15194
  {
@@ -15113,10 +15219,11 @@ ${notes.join("\n")}`,
15113
15219
  const next = [
15114
15220
  codeFile ? `Write the function in ${path9.relative(process.cwd(), codeFile)} and map its inputs under inputs.inputData in softr-workflows.jsonc.` : "",
15115
15221
  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}.`
15222
+ `Then: softr workflows push, softr workflows test ${id}.`
15117
15223
  ].filter(Boolean);
15118
- emit(
15224
+ emitWithVersionNote(
15119
15225
  options,
15226
+ config,
15120
15227
  `Added ${entry.type} "${id}" after "${after}"${opts.inLoop ? ` inside loop "${opts.inLoop}"` : ""}.
15121
15228
  ${next.join("\n")}`,
15122
15229
  {
@@ -15158,58 +15265,66 @@ async function resolveWorkspace(client, requested) {
15158
15265
  );
15159
15266
  }
15160
15267
  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) => {
15162
- const options = globalOptions(command);
15163
- const cwd = process.cwd();
15164
- const dir = path10.resolve(cwd, opts.dir ?? slug(title));
15165
- if (await configExists(dir)) {
15166
- throw new CliError(`${path10.join(dir, CONFIG_FILE_NAME)} already exists.`, "Pick another --dir or title.");
15167
- }
15168
- const trigger = findCatalogEntry(opts.trigger, "trigger");
15169
- const target = resolveTarget();
15170
- const client = await authenticatedClient(target);
15171
- const workspaceId = await resolveWorkspace(client, opts.workspace);
15172
- const created = await createWorkflow(client, {
15173
- workspaceId,
15174
- applicationId: null,
15175
- title,
15176
- thumbnail: null,
15177
- triggers: [{ id: TRIGGER_ID, type: trigger.type, version: trigger.version, inputs: trigger.template }],
15178
- actions: [],
15179
- paths: [],
15180
- // Like the studio: the author's time zone, so schedules and waits mean what they read.
15181
- configuration: { timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC" }
15182
- });
15183
- const workflow = await getWorkflow(client, created.id);
15184
- const config = { $schema: SCHEMA_POINTER, ...toLocalConfig(workflow) };
15185
- const configFile = await writeConfig(dir, config);
15186
- await writeSchemaFile(dir);
15187
- const syncedHash = definitionHash((await packWorkflow(config, dir)).packed);
15188
- await writeState(dir, {
15189
- ...target,
15190
- workflowId: created.id,
15191
- version: workflow.version,
15192
- draft: workflow.draft,
15193
- syncedHash
15194
- });
15195
- const studioUrl = `${target.studioUrl}/workflow/${created.id}`;
15196
- emit(
15197
- options,
15198
- `Created "${title}" (${created.id}) with a ${trigger.type} trigger "${TRIGGER_ID}" in workspace ${workspaceId}.
15199
- ${path10.relative(cwd, configFile)}
15200
- ${studioUrl}
15201
- Next: softr-workflows add <TYPE> --dir ${path10.relative(cwd, dir) || "."} (see "spec" for the types), then push and test.`,
15202
- {
15203
- workflowId: created.id,
15268
+ 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(
15269
+ "--workspace <id>",
15270
+ "workspace to create it in (default: $SOFTR_WORKSPACE_ID, else your only or default workspace)"
15271
+ ).option("--app <id>", "application to link the workflow to (default: $SOFTR_APP_ID when set)").option("--trigger <TYPE>", 'trigger type from "spec" (default WEBHOOK)', "WEBHOOK").option("--dir <path>", "directory to create (default: ./<title-slug>)").action(
15272
+ async (title, opts, command) => {
15273
+ const options = globalOptions(command);
15274
+ const cwd = process.cwd();
15275
+ const dir = path10.resolve(cwd, opts.dir ?? slug(title));
15276
+ if (await configExists(dir)) {
15277
+ throw new CliError(`${path10.join(dir, CONFIG_FILE_NAME)} already exists.`, "Pick another --dir or title.");
15278
+ }
15279
+ const trigger = findCatalogEntry(opts.trigger, "trigger");
15280
+ const target = resolveTarget();
15281
+ const client = await authenticatedClient(target);
15282
+ const workspaceId = await resolveWorkspace(client, opts.workspace ?? process.env["SOFTR_WORKSPACE_ID"]);
15283
+ const pinToApplicationId = opts.app ?? process.env["SOFTR_APP_ID"];
15284
+ const created = await createWorkflow(client, {
15204
15285
  workspaceId,
15286
+ applicationId: null,
15287
+ ...pinToApplicationId ? { pinToApplicationId } : {},
15205
15288
  title,
15206
- triggerId: TRIGGER_ID,
15207
- triggerType: trigger.type,
15208
- dir,
15209
- studioUrl
15210
- }
15211
- );
15212
- });
15289
+ thumbnail: null,
15290
+ triggers: [{ id: TRIGGER_ID, type: trigger.type, version: trigger.version, inputs: trigger.template }],
15291
+ actions: [],
15292
+ paths: [],
15293
+ // Like the studio: the author's time zone, so schedules and waits mean what they read.
15294
+ configuration: { timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC" }
15295
+ });
15296
+ const workflow = await getWorkflow(client, created.id);
15297
+ const config = { $schema: SCHEMA_POINTER, ...toLocalConfig(workflow) };
15298
+ const configFile = await writeConfig(dir, config);
15299
+ await writeSchemaFile(dir);
15300
+ const syncedHash = definitionHash((await packWorkflow(config, dir)).packed);
15301
+ await writeState(dir, {
15302
+ ...target,
15303
+ workflowId: created.id,
15304
+ version: workflow.version,
15305
+ draft: workflow.draft,
15306
+ syncedHash
15307
+ });
15308
+ const studioUrl = `${target.studioUrl}/workflow/${created.id}`;
15309
+ emit(
15310
+ options,
15311
+ `Created "${title}" (${created.id}) with a ${trigger.type} trigger "${TRIGGER_ID}" in workspace ${workspaceId}${pinToApplicationId ? `, linked to application ${pinToApplicationId}` : ""}.
15312
+ ${path10.relative(cwd, configFile)}
15313
+ ${studioUrl}
15314
+ Next: softr workflows add <TYPE> --dir ${path10.relative(cwd, dir) || "."} (see "spec" for the types), then push and test.`,
15315
+ {
15316
+ workflowId: created.id,
15317
+ workspaceId,
15318
+ pinnedToApplicationId: pinToApplicationId ?? null,
15319
+ title,
15320
+ triggerId: TRIGGER_ID,
15321
+ triggerType: trigger.type,
15322
+ dir,
15323
+ studioUrl
15324
+ }
15325
+ );
15326
+ }
15327
+ );
15213
15328
  }
15214
15329
 
15215
15330
  // src/commands/integrations.ts
@@ -15263,12 +15378,15 @@ async function workspaceFor(client, requested, workflowDir) {
15263
15378
  return resolveWorkspace(client, void 0);
15264
15379
  }
15265
15380
  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) => {
15381
+ program3.command("integrations").description("List the workspace's connected integrations, whose ids go into integrationId inputs").option(
15382
+ "--workspace <id>",
15383
+ "workspace to list (default: $SOFTR_WORKSPACE_ID, else the workflow directory's, else your only or default workspace)"
15384
+ ).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
15385
  const options = globalOptions(command);
15268
15386
  const workflowDir = await workflowDirOf(process.cwd(), opts.dir);
15269
15387
  const target = resolveTarget(await readState(workflowDir ?? process.cwd()));
15270
15388
  const client = await authenticatedClient(target);
15271
- const workspaceId = await workspaceFor(client, opts.workspace, workflowDir);
15389
+ const workspaceId = await workspaceFor(client, opts.workspace ?? process.env["SOFTR_WORKSPACE_ID"], workflowDir);
15272
15390
  const wanted = opts.type?.toUpperCase();
15273
15391
  const rows = (await listWorkspaceIntegrations(client, workspaceId)).map(toRow2).filter((row) => !wanted || row.type === wanted).sort(byTypeThenName);
15274
15392
  emit(options, `Workspace ${workspaceId}${wanted ? `, type ${wanted}` : ""}:
@@ -15322,7 +15440,7 @@ async function interactiveLogin(target) {
15322
15440
  if (answer === "2") {
15323
15441
  const token = (await rl.question("Personal API token: ")).trim();
15324
15442
  if (!token) {
15325
- throw new CliError("No token given.", 'Run "softr-workflows login" again.');
15443
+ throw new CliError("No token given.", 'Run "softr login" again.');
15326
15444
  }
15327
15445
  return pastedToken(token);
15328
15446
  }
@@ -15350,7 +15468,7 @@ function waitForCallback(expectedState) {
15350
15468
  }
15351
15469
  const finish = (status, text) => {
15352
15470
  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>`);
15471
+ response.end(`<!doctype html><title>softr</title><p>${text} You can close this tab.</p>`);
15354
15472
  server.close();
15355
15473
  };
15356
15474
  const error = url.searchParams.get("error");
@@ -15377,9 +15495,7 @@ function waitForCallback(expectedState) {
15377
15495
  }
15378
15496
  const timer = setTimeout(() => {
15379
15497
  server.close();
15380
- settle?.reject(
15381
- new CliError("No answer from the browser within 5 minutes.", 'Run "softr-workflows login" again.')
15382
- );
15498
+ settle?.reject(new CliError("No answer from the browser within 5 minutes.", 'Run "softr login" again.'));
15383
15499
  }, CALLBACK_TIMEOUT_MS);
15384
15500
  timer.unref();
15385
15501
  resolveServer({ redirectUri: `http://127.0.0.1:${address.port}/callback`, result });
@@ -15436,6 +15552,11 @@ function registerLogin(program3) {
15436
15552
  ).option("--no-browser", "browser login, but print the URL instead of opening it (for remote shells and agents)").action(async (opts, command) => {
15437
15553
  const options = globalOptions(command);
15438
15554
  const target = resolveTarget();
15555
+ if (process.env[AUTH_ENV_VAR] === "proxy") {
15556
+ throw new CliError(
15557
+ `Login is not needed: the target authenticates the requests itself ($${AUTH_ENV_VAR}=proxy).`
15558
+ );
15559
+ }
15439
15560
  const token = (opts.token ?? process.env[TOKEN_ENV_VAR] ?? "").trim();
15440
15561
  const saved = token ? pastedToken(token) : opts.browser ? await interactiveLogin(target) : await browserLogin(target, true);
15441
15562
  const identity = await identify({ target, token: saved.token, kind: saved.kind });
@@ -15455,6 +15576,14 @@ function registerLogin(program3) {
15455
15576
  const options = globalOptions(command);
15456
15577
  const target = resolveTarget();
15457
15578
  const client = await authenticatedClient(target);
15579
+ if (client.kind === "proxy") {
15580
+ emit(
15581
+ options,
15582
+ `Requests to ${targetHost(target)} carry no token: the target authenticates them itself ($${AUTH_ENV_VAR}=proxy).`,
15583
+ { kind: "proxy", host: targetHost(target) }
15584
+ );
15585
+ return;
15586
+ }
15458
15587
  const identity = await identify(client);
15459
15588
  const stored = process.env[TOKEN_ENV_VAR] ? void 0 : await storedToken(target);
15460
15589
  const expiry = stored?.expiresAt ? ` Expires ${stored.expiresAt.slice(0, 16).replace("T", " ")} UTC.` : "";
@@ -15484,51 +15613,6 @@ function outdatedReferences(row) {
15484
15613
  const outdated = row?.sampleOutput?.outdatedDependencies;
15485
15614
  return outdated && Object.keys(outdated).length ? outdated : {};
15486
15615
  }
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
15616
  function renderIssues(issues) {
15533
15617
  return issues.map((issue) => ` - ${issue.id} (${issue.type}): ${issue.detail}`).join("\n");
15534
15618
  }
@@ -15618,7 +15702,7 @@ async function runOne(client, workflowId, node, opts) {
15618
15702
  if (!node.testable) {
15619
15703
  return { ...base, outcome: "skipped", detail: "branches, filters and loops have no output of their own" };
15620
15704
  }
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);
15705
+ 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
15706
  return { ...base, ...outcomeOf(row), row };
15623
15707
  }
15624
15708
  function mergeRows(existing, fresh) {
@@ -15688,7 +15772,7 @@ function registerTest(program3) {
15688
15772
  "Run a node in the workflows service and save its sample output; --all runs every node in execution order"
15689
15773
  ).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
15774
  "--iteration <n>",
15691
- "for a node inside a loop: which iteration to use",
15775
+ "for a node inside a loop: the iteration of the enclosing loop(s) to use (default 0)",
15692
15776
  (value) => Number.parseInt(value, 10)
15693
15777
  ).option(
15694
15778
  "--sample <n>",
@@ -15744,7 +15828,7 @@ function registerTest(program3) {
15744
15828
  ].join("\n")
15745
15829
  );
15746
15830
  }
15747
- emit(options, [renderResults(results), notes.join("\n")].filter(Boolean).join("\n\n"), {
15831
+ emitWithVersionNote(options, config, [renderResults(results), notes.join("\n")].filter(Boolean).join("\n\n"), {
15748
15832
  workflowId: config.id,
15749
15833
  results: results.map(({ row: _row, ...result }) => result),
15750
15834
  headersRefreshed: headers.refreshed,
@@ -15831,7 +15915,7 @@ function registerOutputs(program3) {
15831
15915
  const row = byId.get(nodeId);
15832
15916
  const sample = row?.sampleOutput?.result?.output;
15833
15917
  if (!row || sample === void 0 || sample === null) {
15834
- throw new CliError(`No sample output for "${nodeId}" yet.`, `Run: softr-workflows test ${nodeId}`);
15918
+ throw new CliError(`No sample output for "${nodeId}" yet.`, `Run: softr workflows test ${nodeId}`);
15835
15919
  }
15836
15920
  const listing = collectVariables(nodeId, sample, row.outputMetadata ?? []);
15837
15921
  emit(options, renderVariables(nodeId, listing.variables, listing.unverified), {
@@ -15869,7 +15953,7 @@ function registerOutputs(program3) {
15869
15953
  refreshedHeaders.length ? `Headers refreshed: ${refreshedHeaders.join(", ")}` : "",
15870
15954
  problems.length ? `Code files that do not parse (headers not refreshed):
15871
15955
  ${problems.map((problem) => ` ${problem}`).join("\n")}` : "",
15872
- 'Run "softr-workflows outputs <nodeId>" for the variables of one node.'
15956
+ 'Run "softr workflows outputs <nodeId>" for the variables of one node.'
15873
15957
  ].filter(Boolean);
15874
15958
  emit(options, `${table.toString()}
15875
15959
 
@@ -15957,7 +16041,7 @@ function registerPlaceholder(program3) {
15957
16041
  }
15958
16042
  throw new CliError(
15959
16043
  `${token} is not backed by test data: ${hint}.`,
15960
- `Run "softr-workflows outputs ${nodeId}" to list every referenceable path.`
16044
+ `Run "softr workflows outputs ${nodeId}" to list every referenceable path.`
15961
16045
  );
15962
16046
  }
15963
16047
  const type = renderJsType(description.type);
@@ -15979,55 +16063,6 @@ type: ${type}${example3} (${status})`, {
15979
16063
  import { stdin as stdin2, stdout } from "node:process";
15980
16064
  import { createInterface as createInterface2 } from "node:readline/promises";
15981
16065
 
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
16066
  // src/project/readiness.ts
16032
16067
  var REASONS = {
16033
16068
  REFERENCED_NODE_UNKNOWN: "no node with that id",
@@ -16074,31 +16109,15 @@ function fromReadiness(readiness) {
16074
16109
  }
16075
16110
  }
16076
16111
  return {
16077
- source: "service",
16078
16112
  validationProblems: readiness.validationProblems,
16079
16113
  testIssues,
16080
16114
  unbacked,
16081
16115
  checked: { version: readiness.version, draft: readiness.draft }
16082
16116
  };
16083
16117
  }
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);
16118
+ async function assess(client, workflowId) {
16119
+ return fromReadiness(await getWorkflowReadiness(client, workflowId));
16100
16120
  }
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
16121
 
16103
16122
  // src/commands/publish.ts
16104
16123
  async function openWorkflow(dir) {
@@ -16134,15 +16153,15 @@ function registerPublish(program3) {
16134
16153
  if (state?.syncedHash && definitionHash(packed) !== state.syncedHash) {
16135
16154
  throw new CliError("Local edits are not pushed yet.", "Run push first, then publish.");
16136
16155
  }
16137
- const [remote, rows] = await Promise.all([getWorkflow(client, id), getWorkflowOutputs(client, id)]);
16156
+ const remote = await getWorkflow(client, id);
16138
16157
  if (state && (remote.version !== state.version || remote.draft !== state.draft) && !opts.force) {
16139
16158
  throw new CliError(
16140
16159
  `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
16160
  'Run "pull --force" to see what would go live, then publish; or "publish --force" to enable it unseen.'
16142
16161
  );
16143
16162
  }
16144
- const assessment = await assess(client, id, packed, rows, remote.lastUpdatedAt);
16145
- if (assessment.validationProblems?.length) {
16163
+ const assessment = await assess(client, id);
16164
+ if (assessment.validationProblems.length) {
16146
16165
  throw new CliError(
16147
16166
  `The service would reject publishing:
16148
16167
  ${assessment.validationProblems.map((problem) => ` - ${problem}`).join("\n")}`,
@@ -16152,8 +16171,7 @@ ${assessment.validationProblems.map((problem) => ` - ${problem}`).join("\n")}`,
16152
16171
  const issues = issuesOf(assessment, config);
16153
16172
  if (issues.length && !opts.force) {
16154
16173
  const text = `The workflow is not fully tested:
16155
- ${renderIssues(issues)}${assessment.source === "client" ? `
16156
- ${CLIENT_SIDE_NOTE}` : ""}`;
16174
+ ${renderIssues(issues)}`;
16157
16175
  if (stdin2.isTTY && stdout.isTTY) {
16158
16176
  const rl = createInterface2({ input: stdin2, output: stdout });
16159
16177
  let answer;
@@ -16168,7 +16186,7 @@ Publish anyway? [y/N] `);
16168
16186
  return;
16169
16187
  }
16170
16188
  } else {
16171
- throw new CliError(text, 'Run "softr-workflows test --all" first, or pass --force to publish anyway.');
16189
+ throw new CliError(text, 'Run "softr workflows test --all" first, or pass --force to publish anyway.');
16172
16190
  }
16173
16191
  }
16174
16192
  await enableWorkflow(client, id);
@@ -16190,20 +16208,8 @@ ${target.studioUrl}/workflow/${id}`,
16190
16208
  const options = globalOptions(command);
16191
16209
  const dir = await findWorkflowDir(process.cwd(), opts.dir);
16192
16210
  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);
16211
+ const [remote, live] = await Promise.all([getWorkflow(client, id), getEnabledVersion(client, id)]);
16212
+ const assessment = await assess(client, id);
16207
16213
  const issues = issuesOf(assessment, config);
16208
16214
  let local = "unknown";
16209
16215
  let problem;
@@ -16224,15 +16230,12 @@ ${target.studioUrl}/workflow/${id}`,
16224
16230
  `Local: ${local}${problem ? ` (${problem})` : ""}${state ? `; last sync version ${state.version}, draft ${state.draft}` : ""}`,
16225
16231
  `Server: version ${remote.version}, draft ${remote.draft}${serverMoved ? " (moved since your last sync: pull --force to take it)" : ""}`,
16226
16232
  `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
- ],
16233
+ `Valid: ${assessment.validationProblems.length ? `${assessment.validationProblems.length} problem(s)
16234
+ ${assessment.validationProblems.map((problem2) => ` - ${problem2}`).join("\n")}` : "the service accepts the workflow for publishing"}`,
16231
16235
  `Tests: ${issues.length ? `${issues.length} issue(s)
16232
- ${renderIssues(issues)}` : "every node tested on current data"}`,
16233
- ...assessment.source === "client" ? [CLIENT_SIDE_NOTE] : []
16236
+ ${renderIssues(issues)}` : "every node tested on current data"}`
16234
16237
  ];
16235
- emit(options, lines.join("\n"), {
16238
+ emitWithVersionNote(options, config, lines.join("\n"), {
16236
16239
  workflowId: id,
16237
16240
  title: remote.title,
16238
16241
  studioUrl: `${target.studioUrl}/workflow/${id}`,
@@ -16242,14 +16245,64 @@ ${renderIssues(issues)}` : "every node tested on current data"}`,
16242
16245
  server: { version: remote.version, draft: remote.draft, moved: serverMoved },
16243
16246
  liveVersion: live?.version ?? null,
16244
16247
  testIssues: issues,
16245
- validationProblems: assessment.validationProblems,
16246
- source: assessment.source
16248
+ validationProblems: assessment.validationProblems
16247
16249
  });
16248
16250
  });
16249
16251
  }
16250
16252
 
16251
16253
  // src/commands/push.ts
16252
16254
  import path12 from "node:path";
16255
+
16256
+ // src/project/placeholder-check.ts
16257
+ var TOKEN = /\{(?:outputs|loopActionGroup)\.[^:{}]+:::[^{}]*\}/g;
16258
+ function* stringsIn(value, path13) {
16259
+ if (typeof value === "string") {
16260
+ yield [path13, value];
16261
+ } else if (Array.isArray(value)) {
16262
+ for (const [index, item] of value.entries()) {
16263
+ yield* stringsIn(item, `${path13}[${index}]`);
16264
+ }
16265
+ } else if (value !== null && typeof value === "object") {
16266
+ for (const [key, item] of Object.entries(value)) {
16267
+ if (key === "actions" || key === "paths") {
16268
+ continue;
16269
+ }
16270
+ yield* stringsIn(item, path13 ? `${path13}.${key}` : key);
16271
+ }
16272
+ }
16273
+ }
16274
+ function checkPlaceholders(config, outputs) {
16275
+ const findings = [];
16276
+ const nodes = [...config.triggers ?? [], ...allActions(config.actions)];
16277
+ for (const node of nodes) {
16278
+ for (const [input, text] of stringsIn(node.inputs ?? {}, "")) {
16279
+ for (const token of new Set(text.match(TOKEN) ?? [])) {
16280
+ if (!parseWholePlaceholder(token)) {
16281
+ continue;
16282
+ }
16283
+ const [description] = describeInputData(
16284
+ { id: node.id, type: node.type, inputs: { inputData: { probe: token } } },
16285
+ config,
16286
+ outputs
16287
+ );
16288
+ if (!description) {
16289
+ continue;
16290
+ }
16291
+ const status = description.verified ? "verified" : description.type.kind === "unknown" ? "unknown" : "declared";
16292
+ findings.push({
16293
+ nodeId: node.id,
16294
+ input,
16295
+ token,
16296
+ status,
16297
+ ...description.note ? { note: description.note } : {}
16298
+ });
16299
+ }
16300
+ }
16301
+ }
16302
+ return findings;
16303
+ }
16304
+
16305
+ // src/commands/push.ts
16253
16306
  function registerPush(program3) {
16254
16307
  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
16308
  const options = globalOptions(command);
@@ -16308,8 +16361,9 @@ function registerPush(program3) {
16308
16361
  syncedHash: hash
16309
16362
  });
16310
16363
  const studioUrl = `${target.studioUrl}/workflow/${config.id}`;
16311
- emit(
16364
+ emitWithVersionNote(
16312
16365
  options,
16366
+ config,
16313
16367
  `Pushed "${packed.title}": version ${result.version}, draft ${result.draft}` + (inlined.length ? ` (${inlined.length} file${inlined.length === 1 ? "" : "s"} inlined)` : "") + `.
16314
16368
  ${studioUrl}` + (unbacked.length ? `
16315
16369
  Warning: ${unbacked.length} placeholder(s) not backed by test data:
@@ -16347,7 +16401,7 @@ function renderList() {
16347
16401
  return [
16348
16402
  table.toString(),
16349
16403
  "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.'
16404
+ 'Run "softr workflows spec <TYPE>" for the inputs, output and an example of one type.'
16351
16405
  ].join("\n");
16352
16406
  }
16353
16407
  function integrationIdFields(entry) {
@@ -16382,7 +16436,7 @@ ${table.toString()}`);
16382
16436
  const idFields = integrationIdFields(entry);
16383
16437
  if (idFields.length) {
16384
16438
  blocks.push(
16385
- `${idFields.join(", ")}: run "softr-workflows integrations" for the ids of the workspace's connected integrations.`
16439
+ `${idFields.join(", ")}: run "softr integrations" for the ids of the workspace's connected integrations.`
16386
16440
  );
16387
16441
  }
16388
16442
  } else {
@@ -16424,6 +16478,75 @@ function registerSpec(program3) {
16424
16478
  });
16425
16479
  }
16426
16480
 
16481
+ // src/commands/upgrade.ts
16482
+ function registerUpgrade(program3) {
16483
+ program3.command("upgrade").description(
16484
+ "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)"
16485
+ ).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) => {
16486
+ const options = globalOptions(command);
16487
+ const dir = await findWorkflowDir(process.cwd(), opts.dir);
16488
+ const config = await readConfig(dir);
16489
+ const outdated = outdatedNodes(config);
16490
+ for (const id of nodeIds) {
16491
+ if (!locateNode(config, id)) {
16492
+ throw new CliError(
16493
+ `No node "${id}" in this workflow.`,
16494
+ 'Node ids are the "id" values in softr-workflows.jsonc.'
16495
+ );
16496
+ }
16497
+ }
16498
+ const wanted = nodeIds.length ? outdated.filter((node) => nodeIds.includes(node.id)) : outdated;
16499
+ const current2 = nodeIds.filter((id) => !outdated.some((node) => node.id === id));
16500
+ const upgraded = [];
16501
+ const manual = [];
16502
+ const edits = [];
16503
+ for (const node of wanted) {
16504
+ const location = locateNode(config, node.id);
16505
+ const next = location ? upgradeOf(location.node) : void 0;
16506
+ if (!location || !next) {
16507
+ manual.push(node);
16508
+ continue;
16509
+ }
16510
+ edits.push({ path: location.path, value: next });
16511
+ upgraded.push(node);
16512
+ }
16513
+ if (edits.length) {
16514
+ await editConfig(dir, edits);
16515
+ }
16516
+ const headers = edits.length ? await refreshCodeHeaders(
16517
+ await readConfig(dir),
16518
+ dir,
16519
+ indexOutputs(await readStateFile(dir, OUTPUTS_FILE) ?? [])
16520
+ ) : { refreshed: [], problems: [] };
16521
+ if (!wanted.length && manual.length === 0 && current2.length === 0) {
16522
+ emit(options, "Every node is on the current version of its type.", { upgraded: [], manual: [], current: [] });
16523
+ return;
16524
+ }
16525
+ if (headers.problems.length) {
16526
+ throw new CliError(
16527
+ `Upgraded, but a code file could not get its new header:
16528
+ ${headers.problems.map((problem) => ` ${problem}`).join("\n")}`,
16529
+ 'Fix the file, then run "pull" to regenerate its header.'
16530
+ );
16531
+ }
16532
+ const lines = [
16533
+ ...upgraded.map((node) => `Upgraded ${node.id}: ${node.type} ${node.version} \u2192 ${node.current}.`),
16534
+ ...manual.map(
16535
+ (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.`
16536
+ ),
16537
+ ...current2.map((id) => `${id} is already on the current version.`),
16538
+ headers.refreshed.length ? `Headers refreshed: ${headers.refreshed.join(", ")}` : "",
16539
+ upgraded.length ? "Then: softr workflows push, softr workflows test --all." : ""
16540
+ ].filter(Boolean);
16541
+ emit(options, lines.join("\n"), {
16542
+ upgraded: upgraded.map((node) => ({ id: node.id, type: node.type, from: node.version, to: node.current })),
16543
+ manual: manual.map((node) => ({ id: node.id, type: node.type, from: node.version, to: node.current })),
16544
+ current: current2,
16545
+ headersRefreshed: headers.refreshed
16546
+ });
16547
+ });
16548
+ }
16549
+
16427
16550
  // src/commands/verify.ts
16428
16551
  function section(title, lines, okText) {
16429
16552
  return { title, ok: lines.length === 0, lines: lines.length ? lines : [okText] };
@@ -16479,7 +16602,7 @@ function registerVerify(program3) {
16479
16602
  `in sync with the server (version ${remote.version}, draft ${remote.draft})`
16480
16603
  );
16481
16604
  let rows = await getWorkflowOutputs(client, config.id);
16482
- let assessment = await assess(client, config.id, packed, rows, remote.lastUpdatedAt);
16605
+ let assessment = await assess(client, config.id);
16483
16606
  let ran = [];
16484
16607
  if (opts.testMissing && sync.ok && files.ok) {
16485
16608
  const wanted = new Set(
@@ -16489,23 +16612,19 @@ function registerVerify(program3) {
16489
16612
  if (nodes.length) {
16490
16613
  ran = await runNodeTests(client, config.id, nodes, {}, true);
16491
16614
  rows = await getWorkflowOutputs(client, config.id);
16492
- assessment = await assess(client, config.id, packed, rows, remote.lastUpdatedAt);
16615
+ assessment = await assess(client, config.id);
16493
16616
  }
16494
16617
  }
16495
16618
  await writeStateFile(dir, OUTPUTS_FILE, mergeRows([], rows));
16496
16619
  const headers = await refreshCodeHeaders(config, dir, indexOutputs(rows));
16497
- if (assessment.checked && (assessment.checked.version !== remote.version || assessment.checked.draft !== remote.draft)) {
16620
+ if (assessment.checked.version !== remote.version || assessment.checked.draft !== remote.draft) {
16498
16621
  syncLines.push(
16499
16622
  `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
16623
  );
16501
16624
  }
16502
16625
  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
16626
  sections.push(
16627
+ section("Validation", assessment.validationProblems, "the service accepts the workflow for publishing"),
16509
16628
  section(
16510
16629
  "Tests",
16511
16630
  assessment.testIssues.map((issue) => `${issue.id} (${issue.type}): ${issue.detail}`),
@@ -16525,17 +16644,15 @@ function registerVerify(program3) {
16525
16644
  (item) => `${item.ok ? "OK " : "NOT"} ${item.title}
16526
16645
  ${item.lines.map((line) => ` ${line}`).join("\n")}`
16527
16646
  ),
16528
- assessment.source === "client" ? CLIENT_SIDE_NOTE : "",
16529
16647
  headers.refreshed.length ? `Headers refreshed: ${headers.refreshed.join(", ")}` : "",
16530
16648
  ready ? "Ready to publish." : `Not ready to publish: ${sections.filter((item) => !item.ok).length} section(s) need attention.`
16531
16649
  ].filter(Boolean).join("\n\n");
16532
- emit(options, human, {
16650
+ emitWithVersionNote(options, config, human, {
16533
16651
  workflowId: config.id,
16534
16652
  ready,
16535
- source: assessment.source,
16536
16653
  files: { ok: files.ok, problems: fileProblems },
16537
16654
  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 },
16655
+ validation: { ok: assessment.validationProblems.length === 0, problems: assessment.validationProblems },
16539
16656
  tests: {
16540
16657
  ok: assessment.testIssues.length === 0,
16541
16658
  issues: assessment.testIssues,
@@ -16549,10 +16666,36 @@ ${item.lines.map((line) => ` ${line}`).join("\n")}`
16549
16666
  });
16550
16667
  }
16551
16668
 
16669
+ // src/commands/workspaces.ts
16670
+ var import_cli_table36 = __toESM(require_cli_table3(), 1);
16671
+ function registerWorkspaces(program3) {
16672
+ program3.command("workspaces").description("The workspaces the stored token reaches, with your role in each").action(async (_opts, command) => {
16673
+ const options = globalOptions(command);
16674
+ const target = resolveTarget();
16675
+ const client = await authenticatedClient(target);
16676
+ const workspaces = await listWorkspaces(client);
16677
+ const table = new import_cli_table36.default({
16678
+ head: ["ID", "Name", "Role", "Default"],
16679
+ colWidths: [38, 34, 14, 9],
16680
+ wordWrap: true,
16681
+ style: { head: [], border: [], "padding-left": 1, "padding-right": 1 }
16682
+ });
16683
+ for (const workspace of workspaces) {
16684
+ table.push([workspace.id, workspace.name, workspace.userRole ?? "", workspace.isDefault ? "yes" : ""]);
16685
+ }
16686
+ emit(
16687
+ options,
16688
+ workspaces.length ? `${table.toString()}
16689
+ Pass --workspace <id> to a command, or set SOFTR_WORKSPACE_ID, to work in one of them.` : "The token reaches no workspace.",
16690
+ { workspaces }
16691
+ );
16692
+ });
16693
+ }
16694
+
16552
16695
  // package.json
16553
16696
  var package_default = {
16554
16697
  name: "@softr/cli",
16555
- version: "0.0.1",
16698
+ version: "0.1.1-se21216.10",
16556
16699
  description: "The Softr command line: build, test and publish Softr workflows from local files; more Softr APIs to come",
16557
16700
  keywords: [
16558
16701
  "softr",
@@ -16562,8 +16705,7 @@ var package_default = {
16562
16705
  license: "UNLICENSED",
16563
16706
  type: "module",
16564
16707
  bin: {
16565
- softr: "dist/cli.mjs",
16566
- "softr-workflows": "dist/cli.mjs"
16708
+ softr: "dist/cli.mjs"
16567
16709
  },
16568
16710
  files: [
16569
16711
  "dist"
@@ -16580,7 +16722,7 @@ var package_default = {
16580
16722
  format: "prettier --write .",
16581
16723
  "format:check": "prettier --check .",
16582
16724
  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"
16725
+ "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
16726
  },
16585
16727
  publishConfig: {
16586
16728
  access: "public"
@@ -16603,20 +16745,23 @@ var package_default = {
16603
16745
  };
16604
16746
 
16605
16747
  // 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");
16748
+ 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
16749
  registerLogin(program2);
16608
- registerList(program2);
16750
+ registerWorkspaces(program2);
16609
16751
  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);
16752
+ var workflows = program2.command("workflows").description("Build, test and publish Softr Workflows from local files (one directory per workflow)");
16753
+ registerList(workflows);
16754
+ registerPull(workflows);
16755
+ registerPush(workflows);
16756
+ registerTest(workflows);
16757
+ registerVerify(workflows);
16758
+ registerOutputs(workflows);
16759
+ registerPlaceholder(workflows);
16760
+ registerInit(workflows);
16761
+ registerAdd(workflows);
16762
+ registerEdit(workflows);
16763
+ registerUpgrade(workflows);
16764
+ registerPublish(workflows);
16765
+ registerSpec(workflows);
16766
+ registerSchema(workflows);
16622
16767
  program2.parseAsync(process.argv).catch(reportError);