@softr/cli 0.1.3 → 0.1.5

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 +95 -37
  2. package/dist/cli.mjs +1388 -609
  3. package/package.json +4 -3
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 Table7 = class extends Array {
1748
+ var Table9 = 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
- Table7.reset = () => debug.reset();
1823
+ Table9.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 = Table7;
1832
+ module.exports = Table9;
1833
1833
  }
1834
1834
  });
1835
1835
 
@@ -2598,14 +2598,14 @@ var Option = class {
2598
2598
  * @param {string} flags
2599
2599
  * @param {string} [description]
2600
2600
  */
2601
- constructor(flags, description) {
2602
- this.flags = flags;
2601
+ constructor(flags2, description) {
2602
+ this.flags = flags2;
2603
2603
  this.description = description || "";
2604
- this.required = flags.includes("<");
2605
- this.optional = flags.includes("[");
2606
- this.variadic = /\w\.\.\.[>\]]$/.test(flags);
2604
+ this.required = flags2.includes("<");
2605
+ this.optional = flags2.includes("[");
2606
+ this.variadic = /\w\.\.\.[>\]]$/.test(flags2);
2607
2607
  this.mandatory = false;
2608
- const optionFlags = splitOptionFlags(flags);
2608
+ const optionFlags = splitOptionFlags(flags2);
2609
2609
  this.short = optionFlags.shortFlag;
2610
2610
  this.long = optionFlags.longFlag;
2611
2611
  this.negate = false;
@@ -2856,12 +2856,12 @@ function camelcase(str) {
2856
2856
  return str2 + word[0].toUpperCase() + word.slice(1);
2857
2857
  });
2858
2858
  }
2859
- function splitOptionFlags(flags) {
2859
+ function splitOptionFlags(flags2) {
2860
2860
  let shortFlag;
2861
2861
  let longFlag;
2862
2862
  const shortFlagExp = /^-[^-]$/;
2863
2863
  const longFlagExp = /^--[^-]/;
2864
- const flagParts = flags.split(/[ |,]+/).concat("guard");
2864
+ const flagParts = flags2.split(/[ |,]+/).concat("guard");
2865
2865
  if (shortFlagExp.test(flagParts[0])) shortFlag = flagParts.shift();
2866
2866
  if (longFlagExp.test(flagParts[0])) longFlag = flagParts.shift();
2867
2867
  if (!shortFlag && shortFlagExp.test(flagParts[0]))
@@ -2872,7 +2872,7 @@ function splitOptionFlags(flags) {
2872
2872
  }
2873
2873
  if (flagParts[0].startsWith("-")) {
2874
2874
  const unsupportedFlag = flagParts[0];
2875
- const baseError = `option creation failed due to '${unsupportedFlag}' in option flags '${flags}'`;
2875
+ const baseError = `option creation failed due to '${unsupportedFlag}' in option flags '${flags2}'`;
2876
2876
  if (/^-[^-][^-]/.test(unsupportedFlag))
2877
2877
  throw new Error(
2878
2878
  `${baseError}
@@ -2891,7 +2891,7 @@ function splitOptionFlags(flags) {
2891
2891
  }
2892
2892
  if (shortFlag === void 0 && longFlag === void 0)
2893
2893
  throw new Error(
2894
- `option creation failed due to no flags found in '${flags}'.`
2894
+ `option creation failed due to no flags found in '${flags2}'.`
2895
2895
  );
2896
2896
  return { shortFlag, longFlag };
2897
2897
  }
@@ -3454,8 +3454,8 @@ Expecting one of '${allowedValues.join("', '")}'`);
3454
3454
  * @param {string} [description]
3455
3455
  * @return {Option} new option
3456
3456
  */
3457
- createOption(flags, description) {
3458
- return new Option(flags, description);
3457
+ createOption(flags2, description) {
3458
+ return new Option(flags2, description);
3459
3459
  }
3460
3460
  /**
3461
3461
  * Wrap parseArgs to catch 'commander.invalidArgument'.
@@ -3570,13 +3570,13 @@ Expecting one of '${allowedValues.join("', '")}'`);
3570
3570
  * @return {Command} `this` command for chaining
3571
3571
  * @private
3572
3572
  */
3573
- _optionEx(config, flags, description, fn, defaultValue) {
3574
- if (typeof flags === "object" && flags instanceof Option) {
3573
+ _optionEx(config, flags2, description, fn, defaultValue) {
3574
+ if (typeof flags2 === "object" && flags2 instanceof Option) {
3575
3575
  throw new Error(
3576
3576
  "To add an Option object use addOption() instead of option() or requiredOption()"
3577
3577
  );
3578
3578
  }
3579
- const option = this.createOption(flags, description);
3579
+ const option = this.createOption(flags2, description);
3580
3580
  option.makeOptionMandatory(!!config.mandatory);
3581
3581
  if (typeof fn === "function") {
3582
3582
  option.default(defaultValue).argParser(fn);
@@ -3613,8 +3613,8 @@ Expecting one of '${allowedValues.join("', '")}'`);
3613
3613
  * @param {*} [defaultValue]
3614
3614
  * @return {Command} `this` command for chaining
3615
3615
  */
3616
- option(flags, description, parseArg, defaultValue) {
3617
- return this._optionEx({}, flags, description, parseArg, defaultValue);
3616
+ option(flags2, description, parseArg, defaultValue) {
3617
+ return this._optionEx({}, flags2, description, parseArg, defaultValue);
3618
3618
  }
3619
3619
  /**
3620
3620
  * Add a required option which must have a value after parsing. This usually means
@@ -3628,10 +3628,10 @@ Expecting one of '${allowedValues.join("', '")}'`);
3628
3628
  * @param {*} [defaultValue]
3629
3629
  * @return {Command} `this` command for chaining
3630
3630
  */
3631
- requiredOption(flags, description, parseArg, defaultValue) {
3631
+ requiredOption(flags2, description, parseArg, defaultValue) {
3632
3632
  return this._optionEx(
3633
3633
  { mandatory: true },
3634
- flags,
3634
+ flags2,
3635
3635
  description,
3636
3636
  parseArg,
3637
3637
  defaultValue
@@ -3798,18 +3798,18 @@ Expecting one of '${allowedValues.join("', '")}'`);
3798
3798
  *
3799
3799
  * @private
3800
3800
  */
3801
- _prepareUserArgs(argv, parseOptions) {
3801
+ _prepareUserArgs(argv, parseOptions2) {
3802
3802
  if (argv !== void 0 && !Array.isArray(argv)) {
3803
3803
  throw new Error("first parameter to parse must be array or undefined");
3804
3804
  }
3805
- parseOptions = parseOptions || {};
3806
- if (argv === void 0 && parseOptions.from === void 0) {
3805
+ parseOptions2 = parseOptions2 || {};
3806
+ if (argv === void 0 && parseOptions2.from === void 0) {
3807
3807
  if (process2.versions?.electron) {
3808
- parseOptions.from = "electron";
3808
+ parseOptions2.from = "electron";
3809
3809
  }
3810
3810
  const execArgv = process2.execArgv ?? [];
3811
3811
  if (execArgv.includes("-e") || execArgv.includes("--eval") || execArgv.includes("-p") || execArgv.includes("--print")) {
3812
- parseOptions.from = "eval";
3812
+ parseOptions2.from = "eval";
3813
3813
  }
3814
3814
  }
3815
3815
  if (argv === void 0) {
@@ -3817,7 +3817,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
3817
3817
  }
3818
3818
  this.rawArgs = argv.slice();
3819
3819
  let userArgs;
3820
- switch (parseOptions.from) {
3820
+ switch (parseOptions2.from) {
3821
3821
  case void 0:
3822
3822
  case "node":
3823
3823
  this._scriptPath = argv[1];
@@ -3839,7 +3839,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
3839
3839
  break;
3840
3840
  default:
3841
3841
  throw new Error(
3842
- `unexpected parse option { from: '${parseOptions.from}' }`
3842
+ `unexpected parse option { from: '${parseOptions2.from}' }`
3843
3843
  );
3844
3844
  }
3845
3845
  if (!this._name && this._scriptPath)
@@ -3869,9 +3869,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
3869
3869
  * @param {string} [parseOptions.from] - where the args are from: 'node', 'user', 'electron'
3870
3870
  * @return {Command} `this` command for chaining
3871
3871
  */
3872
- parse(argv, parseOptions) {
3872
+ parse(argv, parseOptions2) {
3873
3873
  this._prepareForParse();
3874
- const userArgs = this._prepareUserArgs(argv, parseOptions);
3874
+ const userArgs = this._prepareUserArgs(argv, parseOptions2);
3875
3875
  this._parseCommand([], userArgs);
3876
3876
  return this;
3877
3877
  }
@@ -3895,9 +3895,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
3895
3895
  * @param {string} parseOptions.from - where the args are from: 'node', 'user', 'electron'
3896
3896
  * @return {Promise}
3897
3897
  */
3898
- async parseAsync(argv, parseOptions) {
3898
+ async parseAsync(argv, parseOptions2) {
3899
3899
  this._prepareForParse();
3900
- const userArgs = this._prepareUserArgs(argv, parseOptions);
3900
+ const userArgs = this._prepareUserArgs(argv, parseOptions2);
3901
3901
  await this._parseCommand([], userArgs);
3902
3902
  return this;
3903
3903
  }
@@ -4748,12 +4748,12 @@ Expecting one of '${allowedValues.join("', '")}'`);
4748
4748
  * @param {string} [description]
4749
4749
  * @return {(this | string | undefined)} `this` command for chaining, or version string if no arguments
4750
4750
  */
4751
- version(str, flags, description) {
4751
+ version(str, flags2, description) {
4752
4752
  if (str === void 0) return this._version;
4753
4753
  this._version = str;
4754
- flags = flags || "-V, --version";
4754
+ flags2 = flags2 || "-V, --version";
4755
4755
  description = description || "output the version number";
4756
- const versionOption = this.createOption(flags, description);
4756
+ const versionOption = this.createOption(flags2, description);
4757
4757
  this._versionOptionName = versionOption.attributeName();
4758
4758
  this._registerOption(versionOption);
4759
4759
  this.on("option:" + versionOption.name(), () => {
@@ -4951,9 +4951,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
4951
4951
  * @param {string} [path]
4952
4952
  * @return {(string|null|Command)}
4953
4953
  */
4954
- executableDir(path13) {
4955
- if (path13 === void 0) return this._executableDir;
4956
- this._executableDir = path13;
4954
+ executableDir(path15) {
4955
+ if (path15 === void 0) return this._executableDir;
4956
+ this._executableDir = path15;
4957
4957
  return this;
4958
4958
  }
4959
4959
  /**
@@ -5055,9 +5055,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
5055
5055
  * @param {string} [description]
5056
5056
  * @return {Command} `this` command for chaining
5057
5057
  */
5058
- helpOption(flags, description) {
5059
- if (typeof flags === "boolean") {
5060
- if (flags) {
5058
+ helpOption(flags2, description) {
5059
+ if (typeof flags2 === "boolean") {
5060
+ if (flags2) {
5061
5061
  if (this._helpOption === null) this._helpOption = void 0;
5062
5062
  if (this._defaultOptionGroup) {
5063
5063
  this._initOptionGroup(this._getHelpOption());
@@ -5068,10 +5068,10 @@ Expecting one of '${allowedValues.join("', '")}'`);
5068
5068
  return this;
5069
5069
  }
5070
5070
  this._helpOption = this.createOption(
5071
- flags ?? "-h, --help",
5071
+ flags2 ?? "-h, --help",
5072
5072
  description ?? "display help for command"
5073
5073
  );
5074
- if (flags || description) this._initOptionGroup(this._helpOption);
5074
+ if (flags2 || description) this._initOptionGroup(this._helpOption);
5075
5075
  return this;
5076
5076
  }
5077
5077
  /**
@@ -5209,7 +5209,7 @@ function useColor() {
5209
5209
  var program = new Command();
5210
5210
 
5211
5211
  // src/commands/add.ts
5212
- import path9 from "node:path";
5212
+ import path6 from "node:path";
5213
5213
 
5214
5214
  // src/errors.ts
5215
5215
  var CliError = class extends Error {
@@ -5310,12 +5310,12 @@ function executionOrder(config) {
5310
5310
  collect(config.actions, []);
5311
5311
  const outgoing = new Map(appearance.map((id) => [id, []]));
5312
5312
  const indegree = new Map(appearance.map((id) => [id, 0]));
5313
- for (const path13 of pathSets.flat()) {
5314
- const to = path13.toActionId;
5315
- if (!to || !nodes.has(path13.fromActionId) || !nodes.has(to) || outgoing.get(path13.fromActionId).includes(to)) {
5313
+ for (const path15 of pathSets.flat()) {
5314
+ const to = path15.toActionId;
5315
+ if (!to || !nodes.has(path15.fromActionId) || !nodes.has(to) || outgoing.get(path15.fromActionId).includes(to)) {
5316
5316
  continue;
5317
5317
  }
5318
- outgoing.get(path13.fromActionId).push(to);
5318
+ outgoing.get(path15.fromActionId).push(to);
5319
5319
  indegree.set(to, indegree.get(to) + 1);
5320
5320
  }
5321
5321
  const queue = [...nodes.values()].filter((node) => node.kind === "trigger").map((node) => node.id);
@@ -6098,7 +6098,10 @@ var SoftrApiError = class extends Error {
6098
6098
  body;
6099
6099
  hint;
6100
6100
  };
6101
- function hintFor(status, client) {
6101
+ function hintFor(status, client, service) {
6102
+ if (service === "tables") {
6103
+ return tablesHintFor(status, client);
6104
+ }
6102
6105
  if (status === 401 || status === 403) {
6103
6106
  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";
6104
6107
  }
@@ -6107,40 +6110,63 @@ function hintFor(status, client) {
6107
6110
  }
6108
6111
  return void 0;
6109
6112
  }
6110
- function authHeaders(client) {
6111
- return client.kind === "proxy" ? {} : { Authorization: `Bearer ${client.token}` };
6113
+ function tablesHintFor(status, client) {
6114
+ if (status === 401 || status === 403) {
6115
+ return client.kind === "proxy" ? "The target refused the request: no access to this database, or its credential is gone." : "The token was not accepted: revoked, from another environment, or its statements do not grant this databases action (databases.records:read, databases.records:write, databases.schema:write). Run: softr login";
6116
+ }
6117
+ if (status === 429) {
6118
+ return "Too many requests for this token. Wait a moment and try again.";
6119
+ }
6120
+ return void 0;
6121
+ }
6122
+ function authHeaders(client, service) {
6123
+ if (client.kind === "proxy") {
6124
+ return {};
6125
+ }
6126
+ return service === "tables" ? { "Softr-Api-Key": client.token } : { Authorization: `Bearer ${client.token}` };
6112
6127
  }
6113
- async function send(client, service, method, path13, body) {
6128
+ var BASE_URLS = {
6129
+ workflows: (target) => target.apiUrl,
6130
+ studio: (target) => target.studioApiUrl,
6131
+ tables: (target) => target.tablesApiUrl
6132
+ };
6133
+ async function send(client, service, method, path15, body) {
6114
6134
  const init = {
6115
6135
  method,
6116
- headers: { ...authHeaders(client), Accept: "application/json" }
6136
+ headers: { ...authHeaders(client, service), Accept: "application/json" }
6117
6137
  };
6118
6138
  if (body !== void 0) {
6119
6139
  init.headers = { ...init.headers, "Content-Type": "application/json" };
6120
6140
  init.body = JSON.stringify(body);
6121
6141
  }
6122
- const baseUrl = service === "workflows" ? client.target.apiUrl : client.target.studioApiUrl;
6123
- const response = await fetch(`${baseUrl}${path13}`, init);
6142
+ const response = await fetch(`${BASE_URLS[service](client.target)}${path15}`, init);
6124
6143
  if (response.ok) {
6125
6144
  return response;
6126
6145
  }
6127
6146
  const text = await response.text();
6128
6147
  throw new SoftrApiError(
6129
- `${method} ${path13} failed with status ${response.status}${text ? `: ${text}` : ""}`,
6148
+ `${method} ${path15} failed with status ${response.status}${text ? `: ${text}` : ""}`,
6130
6149
  response.status,
6131
6150
  text,
6132
- hintFor(response.status, client)
6151
+ hintFor(response.status, client, service)
6133
6152
  );
6134
6153
  }
6135
- async function apiRequest(client, method, path13, body) {
6136
- const response = await send(client, "workflows", method, path13, body);
6154
+ async function apiRequest(client, method, path15, body) {
6155
+ const response = await send(client, "workflows", method, path15, body);
6156
+ return await response.json();
6157
+ }
6158
+ async function apiRequestVoid(client, method, path15, body) {
6159
+ await send(client, "workflows", method, path15, body);
6160
+ }
6161
+ async function tablesRequest(client, method, path15, body) {
6162
+ const response = await send(client, "tables", method, path15, body);
6137
6163
  return await response.json();
6138
6164
  }
6139
- async function apiRequestVoid(client, method, path13, body) {
6140
- await send(client, "workflows", method, path13, body);
6165
+ async function tablesRequestVoid(client, method, path15, body) {
6166
+ await send(client, "tables", method, path15, body);
6141
6167
  }
6142
- async function studioRequest(client, method, path13, body) {
6143
- const response = await send(client, "studio", method, path13, body);
6168
+ async function studioRequest(client, method, path15, body) {
6169
+ const response = await send(client, "studio", method, path15, body);
6144
6170
  return await response.json();
6145
6171
  }
6146
6172
 
@@ -6151,6 +6177,9 @@ function globalOptions(command) {
6151
6177
  function emit(options, human, data2) {
6152
6178
  process.stdout.write(options.json ? JSON.stringify(data2, null, 2) + "\n" : human + "\n");
6153
6179
  }
6180
+ function plainTable(head, colWidths) {
6181
+ return { head, colWidths, wordWrap: true, style: { head: [], border: [], "padding-left": 1, "padding-right": 1 } };
6182
+ }
6154
6183
  function reportError(error) {
6155
6184
  if (error instanceof CliError || error instanceof SoftrApiError) {
6156
6185
  process.stderr.write(`Error: ${error.message}
@@ -6963,12 +6992,12 @@ function parseTree(text, errors = [], options = ParseOptions.DEFAULT) {
6963
6992
  }
6964
6993
  return result;
6965
6994
  }
6966
- function findNodeAtLocation(root, path13) {
6995
+ function findNodeAtLocation(root, path15) {
6967
6996
  if (!root) {
6968
6997
  return void 0;
6969
6998
  }
6970
6999
  let node = root;
6971
- for (let segment of path13) {
7000
+ for (let segment of path15) {
6972
7001
  if (typeof segment === "string") {
6973
7002
  if (node.type !== "object" || !Array.isArray(node.children)) {
6974
7003
  return void 0;
@@ -7322,14 +7351,14 @@ function getNodeType(value) {
7322
7351
 
7323
7352
  // node_modules/jsonc-parser/lib/esm/impl/edit.js
7324
7353
  function setProperty(text, originalPath, value, options) {
7325
- const path13 = originalPath.slice();
7354
+ const path15 = originalPath.slice();
7326
7355
  const errors = [];
7327
7356
  const root = parseTree(text, errors);
7328
7357
  let parent = void 0;
7329
7358
  let lastSegment = void 0;
7330
- while (path13.length > 0) {
7331
- lastSegment = path13.pop();
7332
- parent = findNodeAtLocation(root, path13);
7359
+ while (path15.length > 0) {
7360
+ lastSegment = path15.pop();
7361
+ parent = findNodeAtLocation(root, path15);
7333
7362
  if (parent === void 0 && value !== void 0) {
7334
7363
  if (typeof lastSegment === "string") {
7335
7364
  value = { [lastSegment]: value };
@@ -7552,8 +7581,8 @@ function printParseErrorCode(code) {
7552
7581
  }
7553
7582
  return "<unknown ParseErrorCode>";
7554
7583
  }
7555
- function modify(text, path13, value, options) {
7556
- return setProperty(text, path13, value, options);
7584
+ function modify(text, path15, value, options) {
7585
+ return setProperty(text, path15, value, options);
7557
7586
  }
7558
7587
  function applyEdits(text, edits) {
7559
7588
  let sortedEdits = edits.slice(0).sort((a, b) => {
@@ -7583,14 +7612,36 @@ var CONFIG_FILE_NAME = "softr-workflows.jsonc";
7583
7612
  function configPath(dir) {
7584
7613
  return path2.join(dir, CONFIG_FILE_NAME);
7585
7614
  }
7586
- async function configExists(dir) {
7615
+ async function fileExists(file) {
7587
7616
  try {
7588
- await access(configPath(dir));
7617
+ await access(file);
7589
7618
  return true;
7590
7619
  } catch {
7591
7620
  return false;
7592
7621
  }
7593
7622
  }
7623
+ async function configExists(dir) {
7624
+ return fileExists(configPath(dir));
7625
+ }
7626
+ function parseJsoncFile(file, text) {
7627
+ const errors = [];
7628
+ const value = parse2(text, errors, { allowTrailingComma: true });
7629
+ if (errors.length) {
7630
+ const first = errors[0];
7631
+ const line = text.slice(0, first.offset).split("\n").length;
7632
+ throw new CliError(`${file}:${line}: ${printParseErrorCode(first.error)}`);
7633
+ }
7634
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
7635
+ throw new CliError(`${file}: the top level must be a JSON object.`);
7636
+ }
7637
+ return value;
7638
+ }
7639
+ async function listSubdirsWith(cwd, holds) {
7640
+ const entries = await readdir(cwd, { withFileTypes: true });
7641
+ const dirs = entries.filter((entry) => entry.isDirectory()).map((entry) => path2.join(cwd, entry.name));
7642
+ const flags2 = await Promise.all(dirs.map((dir) => holds(dir)));
7643
+ return dirs.filter((_, index) => flags2[index]);
7644
+ }
7594
7645
  async function readConfig(dir) {
7595
7646
  const file = configPath(dir);
7596
7647
  let text;
@@ -7605,17 +7656,7 @@ async function readConfig(dir) {
7605
7656
  }
7606
7657
  throw error;
7607
7658
  }
7608
- const errors = [];
7609
- const value = parse2(text, errors, { allowTrailingComma: true });
7610
- if (errors.length) {
7611
- const first = errors[0];
7612
- const line = text.slice(0, first.offset).split("\n").length;
7613
- throw new CliError(`${file}:${line}: ${printParseErrorCode(first.error)}`);
7614
- }
7615
- if (value === null || typeof value !== "object" || Array.isArray(value)) {
7616
- throw new CliError(`${file}: the top level must be a JSON object.`);
7617
- }
7618
- return value;
7659
+ return parseJsoncFile(file, text);
7619
7660
  }
7620
7661
  async function writeConfig(dir, config) {
7621
7662
  const file = configPath(dir);
@@ -7635,10 +7676,7 @@ async function findWorkflowDir(cwd, explicitDir) {
7635
7676
  throw new CliError(`No ${CONFIG_FILE_NAME} in ${cwd}.`, hint);
7636
7677
  }
7637
7678
  async function listWorkflowDirs(cwd) {
7638
- const entries = await readdir(cwd, { withFileTypes: true });
7639
- const dirs = entries.filter((entry) => entry.isDirectory()).map((entry) => path2.join(cwd, entry.name));
7640
- const flags = await Promise.all(dirs.map((dir) => configExists(dir)));
7641
- return dirs.filter((_, index) => flags[index]);
7679
+ return listSubdirsWith(cwd, configExists);
7642
7680
  }
7643
7681
 
7644
7682
  // src/project/config-edit.ts
@@ -8249,12 +8287,12 @@ prototypeAccessors.inAsync.get = function() {
8249
8287
  prototypeAccessors.canAwait.get = function() {
8250
8288
  for (var i2 = this.scopeStack.length - 1; i2 >= 0; i2--) {
8251
8289
  var ref2 = this.scopeStack[i2];
8252
- var flags = ref2.flags;
8253
- if (flags & (SCOPE_CLASS_STATIC_BLOCK | SCOPE_CLASS_FIELD_INIT)) {
8290
+ var flags2 = ref2.flags;
8291
+ if (flags2 & (SCOPE_CLASS_STATIC_BLOCK | SCOPE_CLASS_FIELD_INIT)) {
8254
8292
  return false;
8255
8293
  }
8256
- if (flags & SCOPE_FUNCTION) {
8257
- return (flags & SCOPE_ASYNC) > 0;
8294
+ if (flags2 & SCOPE_FUNCTION) {
8295
+ return (flags2 & SCOPE_ASYNC) > 0;
8258
8296
  }
8259
8297
  }
8260
8298
  return this.inModule && this.options.ecmaVersion >= 13 || this.options.allowAwaitOutsideFunction;
@@ -8270,8 +8308,8 @@ prototypeAccessors.allowReturn.get = function() {
8270
8308
  };
8271
8309
  prototypeAccessors.allowSuper.get = function() {
8272
8310
  var ref2 = this.currentThisScope();
8273
- var flags = ref2.flags;
8274
- return (flags & SCOPE_SUPER) > 0 || this.options.allowSuperOutsideMethod;
8311
+ var flags2 = ref2.flags;
8312
+ return (flags2 & SCOPE_SUPER) > 0 || this.options.allowSuperOutsideMethod;
8275
8313
  };
8276
8314
  prototypeAccessors.allowDirectSuper.get = function() {
8277
8315
  return (this.currentThisScope().flags & SCOPE_DIRECT_SUPER) > 0;
@@ -8282,8 +8320,8 @@ prototypeAccessors.treatFunctionsAsVar.get = function() {
8282
8320
  prototypeAccessors.allowNewDotTarget.get = function() {
8283
8321
  for (var i2 = this.scopeStack.length - 1; i2 >= 0; i2--) {
8284
8322
  var ref2 = this.scopeStack[i2];
8285
- var flags = ref2.flags;
8286
- if (flags & (SCOPE_CLASS_STATIC_BLOCK | SCOPE_CLASS_FIELD_INIT) || flags & SCOPE_FUNCTION && !(flags & SCOPE_ARROW)) {
8323
+ var flags2 = ref2.flags;
8324
+ if (flags2 & (SCOPE_CLASS_STATIC_BLOCK | SCOPE_CLASS_FIELD_INIT) || flags2 & SCOPE_FUNCTION && !(flags2 & SCOPE_ARROW)) {
8287
8325
  return true;
8288
8326
  }
8289
8327
  }
@@ -8291,11 +8329,11 @@ prototypeAccessors.allowNewDotTarget.get = function() {
8291
8329
  };
8292
8330
  prototypeAccessors.allowUsing.get = function() {
8293
8331
  var ref2 = this.currentScope();
8294
- var flags = ref2.flags;
8295
- if (flags & SCOPE_SWITCH) {
8332
+ var flags2 = ref2.flags;
8333
+ if (flags2 & SCOPE_SWITCH) {
8296
8334
  return false;
8297
8335
  }
8298
- if (!this.inModule && flags & SCOPE_TOP) {
8336
+ if (!this.inModule && flags2 & SCOPE_TOP) {
8299
8337
  return false;
8300
8338
  }
8301
8339
  return true;
@@ -11076,14 +11114,14 @@ pp$4.curPosition = function() {
11076
11114
  }
11077
11115
  };
11078
11116
  var pp$3 = Parser.prototype;
11079
- var Scope = function Scope2(flags) {
11080
- this.flags = flags;
11117
+ var Scope = function Scope2(flags2) {
11118
+ this.flags = flags2;
11081
11119
  this.var = [];
11082
11120
  this.lexical = [];
11083
11121
  this.functions = [];
11084
11122
  };
11085
- pp$3.enterScope = function(flags) {
11086
- this.scopeStack.push(new Scope(flags));
11123
+ pp$3.enterScope = function(flags2) {
11124
+ this.scopeStack.push(new Scope(flags2));
11087
11125
  };
11088
11126
  pp$3.exitScope = function() {
11089
11127
  this.scopeStack.pop();
@@ -11299,12 +11337,12 @@ var RegExpValidationState = function RegExpValidationState2(parser) {
11299
11337
  this.backReferenceNames = [];
11300
11338
  this.branchID = null;
11301
11339
  };
11302
- RegExpValidationState.prototype.reset = function reset(start, pattern, flags) {
11303
- var unicodeSets = flags.indexOf("v") !== -1;
11304
- var unicode = flags.indexOf("u") !== -1;
11340
+ RegExpValidationState.prototype.reset = function reset(start, pattern, flags2) {
11341
+ var unicodeSets = flags2.indexOf("v") !== -1;
11342
+ var unicode = flags2.indexOf("u") !== -1;
11305
11343
  this.start = start | 0;
11306
11344
  this.source = pattern + "";
11307
- this.flags = flags;
11345
+ this.flags = flags2;
11308
11346
  if (unicodeSets && this.parser.options.ecmaVersion >= 15) {
11309
11347
  this.switchU = true;
11310
11348
  this.switchV = true;
@@ -11381,15 +11419,15 @@ RegExpValidationState.prototype.eatChars = function eatChars(chs, forceU) {
11381
11419
  };
11382
11420
  pp$1.validateRegExpFlags = function(state) {
11383
11421
  var validFlags = state.validFlags;
11384
- var flags = state.flags;
11422
+ var flags2 = state.flags;
11385
11423
  var u = false;
11386
11424
  var v = false;
11387
- for (var i2 = 0; i2 < flags.length; i2++) {
11388
- var flag = flags.charAt(i2);
11425
+ for (var i2 = 0; i2 < flags2.length; i2++) {
11426
+ var flag = flags2.charAt(i2);
11389
11427
  if (validFlags.indexOf(flag) === -1) {
11390
11428
  this.raise(state.start, "Invalid regular expression flag");
11391
11429
  }
11392
- if (flags.indexOf(flag, i2 + 1) > -1) {
11430
+ if (flags2.indexOf(flag, i2 + 1) > -1) {
11393
11431
  this.raise(state.start, "Duplicate regular expression flag");
11394
11432
  }
11395
11433
  if (flag === "u") {
@@ -13001,20 +13039,20 @@ pp.readRegexp = function() {
13001
13039
  var pattern = this.input.slice(start, this.pos);
13002
13040
  ++this.pos;
13003
13041
  var flagsStart = this.pos;
13004
- var flags = this.readWord1();
13042
+ var flags2 = this.readWord1();
13005
13043
  if (this.containsEsc) {
13006
13044
  this.unexpected(flagsStart);
13007
13045
  }
13008
13046
  var state = this.regexpState || (this.regexpState = new RegExpValidationState(this));
13009
- state.reset(start, pattern, flags);
13047
+ state.reset(start, pattern, flags2);
13010
13048
  this.validateRegExpFlags(state);
13011
13049
  this.validateRegExpPattern(state);
13012
13050
  var value = null;
13013
13051
  try {
13014
- value = new RegExp(pattern, flags);
13052
+ value = new RegExp(pattern, flags2);
13015
13053
  } catch (e) {
13016
13054
  }
13017
- return this.finishToken(types$1.regexp, { pattern, flags, value });
13055
+ return this.finishToken(types$1.regexp, { pattern, flags: flags2, value });
13018
13056
  };
13019
13057
  pp.readInt = function(radix, len, maybeLegacyOctalNumericLiteral) {
13020
13058
  var allowSeparators = this.options.ecmaVersion >= 12 && len === void 0;
@@ -13555,8 +13593,8 @@ function typeOfField(field) {
13555
13593
  return field.allowMultipleEntries ? arrayOf(base) : base;
13556
13594
  }
13557
13595
  var SEGMENT = /^(?:\.?\[\s*'((?:[^'\\]|\\.)*)'\s*\]|\.?\[\s*"((?:[^"\\]|\\.)*)"\s*\]|\.?\[\s*(\d+)\s*\]|\.?\[\s*\*\s*\]|\.([^.[\]]+))/;
13558
- function parsePath(path13) {
13559
- let rest = path13.trim();
13596
+ function parsePath(path15) {
13597
+ let rest = path15.trim();
13560
13598
  if (rest.startsWith("$")) {
13561
13599
  rest = rest.slice(1);
13562
13600
  }
@@ -14056,10 +14094,10 @@ function describePlaceholder(placeholder, raw, config, outputs, depth) {
14056
14094
  function untested(nodeId) {
14057
14095
  return `node "${nodeId}" has no test output yet (run: softr workflows test ${nodeId})`;
14058
14096
  }
14059
- function describeOutputRef(nodeId, path13, source, outputs) {
14060
- const segments = parsePath(path13);
14097
+ function describeOutputRef(nodeId, path15, source, outputs) {
14098
+ const segments = parsePath(path15);
14061
14099
  if (!segments) {
14062
- return { type: UNKNOWN, source, verified: false, note: `unsupported path "${path13}"` };
14100
+ return { type: UNKNOWN, source, verified: false, note: `unsupported path "${path15}"` };
14063
14101
  }
14064
14102
  const row = outputs[nodeId];
14065
14103
  if (!row) {
@@ -14384,65 +14422,197 @@ ${notes.join("\n")}` : ""}`,
14384
14422
  });
14385
14423
  }
14386
14424
 
14387
- // src/commands/pull.ts
14388
- import path8 from "node:path";
14425
+ // src/project/slug.ts
14426
+ function slug(title, fallback = "workflow") {
14427
+ return title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || fallback;
14428
+ }
14389
14429
 
14390
- // src/api/workflows.ts
14391
- function getWorkflow(client, workflowId) {
14392
- return apiRequest(client, "GET", `/v1/workflows/${workflowId}`);
14430
+ // src/commands/add.ts
14431
+ function uniqueId(base, taken) {
14432
+ if (!taken.has(base)) {
14433
+ return base;
14434
+ }
14435
+ for (let n = 2; ; n++) {
14436
+ if (!taken.has(`${base}-${n}`)) {
14437
+ return `${base}-${n}`;
14438
+ }
14439
+ }
14393
14440
  }
14394
- function updateWorkflow(client, workflowId, body) {
14395
- return apiRequest(client, "PUT", `/v1/workflows/${workflowId}`, body);
14441
+ function defaultAfter(config, loopId) {
14442
+ const order = executionOrder(config);
14443
+ const container = loopId ? order.filter((node) => node.loopId === loopId) : order.filter((node) => node.loopId === void 0 && node.reachable);
14444
+ if (!container.length) {
14445
+ if (loopId) {
14446
+ return loopId;
14447
+ }
14448
+ const trigger = config.triggers?.[0];
14449
+ if (!trigger) {
14450
+ throw new CliError("The workflow has no trigger to attach the first action to.");
14451
+ }
14452
+ return trigger.id;
14453
+ }
14454
+ const paths = loopId ? [...allActions(config.actions)].find((node) => node.id === loopId)?.inputs?.paths ?? [] : config.paths ?? [];
14455
+ const withOutgoing = new Set(paths.map((p) => p.fromActionId));
14456
+ const leaves = container.filter((node) => !withOutgoing.has(node.id));
14457
+ if (leaves.length === 1 && leaves[0]) {
14458
+ return leaves[0].id;
14459
+ }
14460
+ throw new CliError(
14461
+ leaves.length ? `Several nodes have no successor: ${leaves.map((node) => node.id).join(", ")}.` : "Every node already has a successor.",
14462
+ "Pass --after <nodeId> to say where the new node goes."
14463
+ );
14396
14464
  }
14397
- function getWorkflowOutputs(client, workflowId) {
14398
- return apiRequest(client, "GET", `/v1/workflows/${workflowId}/outputs`);
14465
+ function registerAdd(program3) {
14466
+ program3.command("add").description("Add an action after an existing node, or a trigger; custom code gets its own file under actions/").argument("<type>", 'node type from "spec", e.g. CUSTOM_CODE or WEBHOOK').option("--id <id>", "node id, readable and unique (default: derived from the type)").option("--title <title>", "node title shown in the studio (default: the type label)").option("--after <nodeId>", "actions: the node the new one follows (default: the only node without a successor)").option("--to <nodeId>", "triggers: an action the new trigger leads to").option("--in-loop <loopId>", "put the node inside this loop body").option("--lang <language>", "for CUSTOM_CODE: JAVASCRIPT or PYTHON (default JAVASCRIPT)").option("--dir <path>", "workflow directory (default: the current directory)").action(async (type, opts, command) => {
14467
+ const options = globalOptions(command);
14468
+ const dir = await findWorkflowDir(process.cwd(), opts.dir);
14469
+ const config = await readConfig(dir);
14470
+ const entry = findCatalogEntry(type);
14471
+ const nodes = [...config.triggers ?? [], ...allActions(config.actions)];
14472
+ const taken = new Set(nodes.map((node2) => node2.id));
14473
+ const id = opts.id ?? uniqueId(entry.kind === "trigger" ? "trigger" : slug(entry.label), taken);
14474
+ if (taken.has(id)) {
14475
+ throw new CliError(`Node id "${id}" is already used.`);
14476
+ }
14477
+ if (id.includes(":::") || /[\s/?#]/.test(id)) {
14478
+ throw new CliError(`Node id "${id}" must not contain ":::", spaces, "/", "?" or "#".`);
14479
+ }
14480
+ const title = opts.title ?? entry.label;
14481
+ const { inputs, codeFile } = await newNodeInputs(entry, id, title, opts.lang, dir);
14482
+ const node = { id, title, type: entry.type, version: entry.version, inputs };
14483
+ if (entry.kind === "trigger") {
14484
+ if (opts.after || opts.inLoop) {
14485
+ throw new CliError(
14486
+ "Triggers start the workflow: use --to <actionId> to connect one, not --after or --in-loop."
14487
+ );
14488
+ }
14489
+ if (opts.to && !taken.has(opts.to)) {
14490
+ throw new CliError(`No node "${opts.to}" to connect to.`, `Known nodes: ${[...taken].join(", ")}`);
14491
+ }
14492
+ await editConfig(dir, [
14493
+ { path: ["triggers", -1], value: node },
14494
+ ...opts.to ? [{ path: ["paths", -1], value: { fromActionId: id, toActionId: opts.to } }] : []
14495
+ ]);
14496
+ const notes = [
14497
+ (config.triggers?.length ?? 0) >= 1 ? "The service accepts one trigger per published workflow for now; multi-trigger support is coming." : "",
14498
+ opts.to ? "" : `Connect it with a path to the first action (or pass --to next time).`,
14499
+ `Then: softr workflows push, softr workflows test ${id}.`
14500
+ ].filter(Boolean);
14501
+ emitWithVersionNote(
14502
+ options,
14503
+ config,
14504
+ `Added ${entry.type} trigger "${id}"${opts.to ? ` leading to "${opts.to}"` : ""}.
14505
+ ${notes.join("\n")}`,
14506
+ {
14507
+ id,
14508
+ kind: "trigger",
14509
+ type: entry.type,
14510
+ to: opts.to ?? null
14511
+ }
14512
+ );
14513
+ return;
14514
+ }
14515
+ if (opts.inLoop) {
14516
+ const loop = [...allActions(config.actions)].find((candidate) => candidate.id === opts.inLoop);
14517
+ if (!loop || loop.type !== LOOP_TYPE) {
14518
+ throw new CliError(`"${opts.inLoop}" is not a loop in this workflow.`);
14519
+ }
14520
+ }
14521
+ const after = opts.after ?? defaultAfter(config, opts.inLoop);
14522
+ if (!taken.has(after)) {
14523
+ throw new CliError(`No node "${after}" to attach to.`, `Known nodes: ${[...taken].join(", ")}`);
14524
+ }
14525
+ const loopIndex = opts.inLoop ? (config.actions ?? []).findIndex((action) => action.id === opts.inLoop) : -1;
14526
+ const base = opts.inLoop && loopIndex >= 0 ? ["actions", loopIndex, "inputs"] : [];
14527
+ await editConfig(dir, [
14528
+ { path: [...base, "actions", -1], value: node },
14529
+ { path: [...base, "paths", -1], value: { fromActionId: after, toActionId: id } }
14530
+ ]);
14531
+ const next = [
14532
+ codeFile ? `Write the function in ${path6.relative(process.cwd(), codeFile)} and map its inputs under inputs.inputData in softr-workflows.jsonc.` : "",
14533
+ entry.type === "BRANCH" || entry.type === "FILTER" ? `Add the condition(s) on the path(s) leaving it in softr-workflows.jsonc (see "spec ${entry.type}").` : "",
14534
+ `Then: softr workflows push, softr workflows test ${id}.`
14535
+ ].filter(Boolean);
14536
+ emitWithVersionNote(
14537
+ options,
14538
+ config,
14539
+ `Added ${entry.type} "${id}" after "${after}"${opts.inLoop ? ` inside loop "${opts.inLoop}"` : ""}.
14540
+ ${next.join("\n")}`,
14541
+ {
14542
+ id,
14543
+ kind: "action",
14544
+ type: entry.type,
14545
+ after,
14546
+ loopId: opts.inLoop ?? null,
14547
+ codeFile: codeFile ? path6.relative(process.cwd(), codeFile) : null
14548
+ }
14549
+ );
14550
+ });
14399
14551
  }
14400
- function listWorkflows(client, workspaceId, { applicationId } = {}) {
14401
- const query = new URLSearchParams({ workspaceId, ...applicationId ? { applicationId } : {}, limit: "500" });
14402
- return apiRequest(client, "GET", `/v1/workflows?${query}`);
14552
+
14553
+ // src/commands/db/databases.ts
14554
+ var import_cli_table3 = __toESM(require_cli_table3(), 1);
14555
+ import path11 from "node:path";
14556
+
14557
+ // src/api/tables.ts
14558
+ var DATABASES = "/api/v1/databases";
14559
+ async function listDatabases(client) {
14560
+ return (await tablesRequest(client, "GET", DATABASES)).data;
14403
14561
  }
14404
- function pinWorkflow(client, workflowId, applicationId) {
14405
- return apiRequestVoid(client, "POST", `/v1/workflows/${workflowId}/pinned-applications/${applicationId}`);
14562
+ async function getDatabase(client, databaseId) {
14563
+ return (await tablesRequest(client, "GET", `${DATABASES}/${encodeURIComponent(databaseId)}`)).data;
14406
14564
  }
14407
- function unpinWorkflow(client, workflowId, applicationId) {
14408
- return apiRequestVoid(client, "DELETE", `/v1/workflows/${workflowId}/pinned-applications/${applicationId}`);
14565
+ async function createDatabase(client, database) {
14566
+ return (await tablesRequest(client, "POST", DATABASES, database)).data;
14409
14567
  }
14410
- function testAction(client, workflowId, hierarchyPath2, mode) {
14411
- return apiRequest(client, "POST", `/v1/workflows/${workflowId}/outputs/${mode}/actions/${hierarchyPath2}`, {});
14568
+ async function updateDatabase(client, databaseId, changes) {
14569
+ return (await tablesRequest(client, "PUT", `${DATABASES}/${encodeURIComponent(databaseId)}`, changes)).data;
14412
14570
  }
14413
- function testTrigger(client, workflowId, triggerId) {
14414
- return apiRequest(client, "POST", `/v1/workflows/${workflowId}/outputs/triggers/${triggerId}/test`, {});
14571
+ async function deleteDatabase(client, databaseId, force) {
14572
+ await tablesRequestVoid(
14573
+ client,
14574
+ "DELETE",
14575
+ `${DATABASES}/${encodeURIComponent(databaseId)}${force ? "?force=true" : ""}`
14576
+ );
14415
14577
  }
14416
- function saveTriggerSample(client, workflowId, triggerId, sample) {
14417
- return apiRequestVoid(client, "POST", `/v1/workflows/${workflowId}/sample-output/triggers/${triggerId}`, sample);
14578
+ async function listTables(client, databaseId) {
14579
+ return (await tablesRequest(client, "GET", `${DATABASES}/${encodeURIComponent(databaseId)}/tables`)).data;
14418
14580
  }
14419
- function createWorkflow(client, body) {
14420
- return apiRequest(client, "POST", "/v1/workflows", body);
14581
+ function tablePath(databaseId, tableId) {
14582
+ const tables = `${DATABASES}/${encodeURIComponent(databaseId)}/tables`;
14583
+ return tableId ? `${tables}/${encodeURIComponent(tableId)}` : tables;
14421
14584
  }
14422
- function enableWorkflow(client, workflowId) {
14423
- return apiRequestVoid(client, "PUT", `/v1/workflows/${workflowId}/enabled`);
14585
+ async function createTable(client, databaseId, table) {
14586
+ return (await tablesRequest(client, "POST", tablePath(databaseId), table)).data;
14424
14587
  }
14425
- function disableWorkflow(client, workflowId) {
14426
- return apiRequestVoid(client, "DELETE", `/v1/workflows/${workflowId}/enabled`);
14588
+ async function updateTable(client, databaseId, tableId, table) {
14589
+ return (await tablesRequest(client, "PUT", tablePath(databaseId, tableId), table)).data;
14427
14590
  }
14428
- async function getEnabledVersion(client, workflowId) {
14429
- try {
14430
- return await apiRequest(client, "GET", `/v1/workflows/${workflowId}/enabled`);
14431
- } catch (error) {
14432
- if (error instanceof SoftrApiError && error.status === 404) {
14433
- return null;
14434
- }
14435
- throw error;
14436
- }
14591
+ async function deleteTable(client, databaseId, tableId, force) {
14592
+ await tablesRequestVoid(client, "DELETE", `${tablePath(databaseId, tableId)}${force ? "?force=true" : ""}`);
14437
14593
  }
14438
- function getWorkflowReadiness(client, workflowId) {
14439
- return apiRequest(client, "GET", `/v1/workflows/${workflowId}/readiness`);
14594
+ async function addField(client, databaseId, tableId, field) {
14595
+ return (await tablesRequest(client, "POST", `${tablePath(databaseId, tableId)}/fields`, field)).data;
14596
+ }
14597
+ async function updateField(client, databaseId, tableId, fieldId, field) {
14598
+ return (await tablesRequest(
14599
+ client,
14600
+ "PUT",
14601
+ `${tablePath(databaseId, tableId)}/fields/${encodeURIComponent(fieldId)}`,
14602
+ field
14603
+ )).data;
14604
+ }
14605
+ async function deleteField(client, databaseId, tableId, fieldId) {
14606
+ await tablesRequestVoid(client, "DELETE", `${tablePath(databaseId, tableId)}/fields/${encodeURIComponent(fieldId)}`);
14607
+ }
14608
+ async function listViews(client, databaseId, tableId) {
14609
+ return (await tablesRequest(client, "GET", `${tablePath(databaseId, tableId)}/views`)).data;
14440
14610
  }
14441
14611
 
14442
14612
  // src/config/credentials.ts
14443
14613
  import { chmod, mkdir as mkdir4, readFile as readFile5, writeFile as writeFile5 } from "node:fs/promises";
14444
14614
  import { homedir } from "node:os";
14445
- import path6 from "node:path";
14615
+ import path7 from "node:path";
14446
14616
 
14447
14617
  // src/api/oauth.ts
14448
14618
  import { createHash as createHash2, randomBytes } from "node:crypto";
@@ -14475,8 +14645,8 @@ function refreshFailureHint(status) {
14475
14645
  return status === 401 ? "The OAuth grant is gone (revoked or older than 30 days). Run: softr login" : "The studio could not renew the login right now; try again in a moment.";
14476
14646
  }
14477
14647
  async function tokenRequest(target, form) {
14478
- const path13 = "/v1/oauth/token";
14479
- const response = await fetch(`${target.studioApiUrl}${path13}`, {
14648
+ const path15 = "/v1/oauth/token";
14649
+ const response = await fetch(`${target.studioApiUrl}${path15}`, {
14480
14650
  method: "POST",
14481
14651
  headers: { "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json" },
14482
14652
  body: new URLSearchParams({ ...form, client_id: oauthClientId() }).toString()
@@ -14484,7 +14654,7 @@ async function tokenRequest(target, form) {
14484
14654
  if (!response.ok) {
14485
14655
  const text = await response.text();
14486
14656
  throw new SoftrApiError(
14487
- `POST ${path13} failed with status ${response.status}${text ? `: ${text}` : ""}`,
14657
+ `POST ${path15} failed with status ${response.status}${text ? `: ${text}` : ""}`,
14488
14658
  response.status,
14489
14659
  text,
14490
14660
  form["grant_type"] === "refresh_token" ? refreshFailureHint(response.status) : void 0
@@ -14513,9 +14683,11 @@ function refreshTokens(target, refreshToken) {
14513
14683
  var API_URL_ENV_VAR = "SOFTR_WORKFLOWS_API_URL";
14514
14684
  var STUDIO_API_URL_ENV_VAR = "SOFTR_STUDIO_API_URL";
14515
14685
  var STUDIO_URL_ENV_VAR = "SOFTR_STUDIO_URL";
14686
+ var TABLES_API_URL_ENV_VAR = "SOFTR_TABLES_API_URL";
14516
14687
  var DEFAULT_API_URL = "https://workflows-api.softr.io";
14517
14688
  var DEFAULT_STUDIO_API_URL = "https://studio-api.softr.io";
14518
14689
  var DEFAULT_STUDIO_URL = "https://studio.softr.io";
14690
+ var DEFAULT_TABLES_API_URL = "https://tables-api.softr.io";
14519
14691
  function normalizeUrl(value, source) {
14520
14692
  let url;
14521
14693
  try {
@@ -14533,7 +14705,8 @@ function resolveTarget(remembered) {
14533
14705
  return {
14534
14706
  apiUrl: pick(API_URL_ENV_VAR, remembered?.apiUrl, DEFAULT_API_URL),
14535
14707
  studioApiUrl: pick(STUDIO_API_URL_ENV_VAR, remembered?.studioApiUrl, DEFAULT_STUDIO_API_URL),
14536
- studioUrl: pick(STUDIO_URL_ENV_VAR, remembered?.studioUrl, DEFAULT_STUDIO_URL)
14708
+ studioUrl: pick(STUDIO_URL_ENV_VAR, remembered?.studioUrl, DEFAULT_STUDIO_URL),
14709
+ tablesApiUrl: pick(TABLES_API_URL_ENV_VAR, remembered?.tablesApiUrl, DEFAULT_TABLES_API_URL)
14537
14710
  };
14538
14711
  }
14539
14712
  function targetHost(target) {
@@ -14563,7 +14736,7 @@ function isProxyAuth() {
14563
14736
  }
14564
14737
  var REFRESH_MARGIN_MS = 5 * 60 * 1e3;
14565
14738
  function credentialsPath() {
14566
- return path6.join(homedir(), ".softr", "credentials.json");
14739
+ return path7.join(homedir(), ".softr", "credentials.json");
14567
14740
  }
14568
14741
  async function readCredentials() {
14569
14742
  try {
@@ -14577,7 +14750,7 @@ async function readCredentials() {
14577
14750
  }
14578
14751
  async function writeCredentials(credentials) {
14579
14752
  const file = credentialsPath();
14580
- await mkdir4(path6.dirname(file), { recursive: true, mode: 448 });
14753
+ await mkdir4(path7.dirname(file), { recursive: true, mode: 448 });
14581
14754
  await writeFile5(file, JSON.stringify(credentials, null, 2) + "\n", { mode: 384 });
14582
14755
  await chmod(file, 384);
14583
14756
  return file;
@@ -14650,66 +14823,64 @@ async function authenticatedClient(target) {
14650
14823
  return { target, token: stored.token, kind };
14651
14824
  }
14652
14825
 
14653
- // src/commands/list.ts
14654
- var import_cli_table3 = __toESM(require_cli_table3(), 1);
14826
+ // src/project/db-dir.ts
14827
+ import { mkdir as mkdir5, readdir as readdir2, readFile as readFile6, rm, writeFile as writeFile6 } from "node:fs/promises";
14828
+ import path8 from "node:path";
14655
14829
 
14656
- // src/api/studio.ts
14657
- async function resolveTokenOwner(client) {
14658
- try {
14659
- return await studioRequest(client, "POST", "/v1/users/me/token", { token: client.token });
14660
- } catch (error) {
14661
- if (error instanceof SoftrApiError && error.status === 404) {
14662
- throw new CliError(
14663
- "The studio does not know this token: revoked, expired, or from another environment.",
14664
- 'Create a new personal API token in the studio, or run "softr login" for the browser login.'
14665
- );
14666
- }
14667
- throw error;
14830
+ // src/project/lookup.ts
14831
+ function byIdOrName(items, reference, kind, describe2 = (item) => item.id) {
14832
+ const byId = items.find((item) => item.id === reference);
14833
+ if (byId) {
14834
+ return byId;
14835
+ }
14836
+ const byName = items.filter((item) => item.name === reference);
14837
+ if (byName.length > 1) {
14838
+ throw new CliError(
14839
+ `Several ${kind}s are named "${reference}".`,
14840
+ `Pass the id of one: ${byName.map(describe2).join(", ")}`
14841
+ );
14668
14842
  }
14843
+ return byName[0];
14669
14844
  }
14670
- function grantsWorkflows(statements) {
14671
- return (statements ?? []).some(
14672
- (statement) => statement.actions.some((action) => action === "*" || action.startsWith("workflows:"))
14673
- );
14674
- }
14675
- async function listWorkspaces(client) {
14676
- const workspaces = await studioRequest(client, "GET", "/v1/api/me/workspaces");
14677
- return workspaces.map((workspace) => ({
14678
- id: workspace.id,
14679
- name: workspace.name,
14680
- ...workspace.role ? { userRole: workspace.role } : {},
14681
- isDefault: workspace.isDefault
14682
- }));
14845
+
14846
+ // src/project/db-dir.ts
14847
+ var DB_CONFIG_FILE_NAME = "softr-db.jsonc";
14848
+ var DB_CACHE_DIR_NAME = ".softr-db";
14849
+ var DATABASE_ID_ENV_VAR = "SOFTR_DATABASE_ID";
14850
+ function configFile(dir) {
14851
+ return path8.join(dir, DB_CONFIG_FILE_NAME);
14683
14852
  }
14684
- function listWorkspaceIntegrations(client, workspaceId) {
14685
- return studioRequest(client, "GET", `/v1/api/me/workspaces/${workspaceId}/integrations`);
14853
+ function cacheFile(dir) {
14854
+ return path8.join(dir, DB_CACHE_DIR_NAME, "schema.json");
14686
14855
  }
14687
-
14688
- // src/project/state.ts
14689
- import { mkdir as mkdir5, readFile as readFile6, writeFile as writeFile6 } from "node:fs/promises";
14690
- import path7 from "node:path";
14691
- var STATE_DIR_NAME = ".softr-workflows";
14692
- function stateDir(dir) {
14693
- return path7.join(dir, STATE_DIR_NAME);
14856
+ async function dbConfigExists(dir) {
14857
+ return fileExists(configFile(dir));
14694
14858
  }
14695
- async function readState(dir) {
14859
+ async function readDbConfig(dir) {
14860
+ const file = configFile(dir);
14861
+ let text;
14696
14862
  try {
14697
- return JSON.parse(await readFile6(path7.join(stateDir(dir), "state.json"), "utf8"));
14863
+ text = await readFile6(file, "utf8");
14698
14864
  } catch (error) {
14699
14865
  if (error.code === "ENOENT") {
14700
14866
  return void 0;
14701
14867
  }
14702
14868
  throw error;
14703
14869
  }
14870
+ const value = parseJsoncFile(file, text);
14871
+ if (typeof value["databaseId"] !== "string" || !value["databaseId"]) {
14872
+ throw new CliError(`${file}: "databaseId" must be a non-empty string.`);
14873
+ }
14874
+ return { databaseId: value["databaseId"] };
14704
14875
  }
14705
- async function writeState(dir, state) {
14706
- await mkdir5(stateDir(dir), { recursive: true });
14707
- const value = { ...state, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
14708
- await writeFile6(path7.join(stateDir(dir), "state.json"), JSON.stringify(value, null, 2) + "\n");
14876
+ async function writeDbConfig(dir, config) {
14877
+ await mkdir5(dir, { recursive: true });
14878
+ await writeFile6(configFile(dir), JSON.stringify(config, null, 2) + "\n");
14879
+ return configFile(dir);
14709
14880
  }
14710
- async function readStateFile(dir, name) {
14881
+ async function readSchemaCache(dir) {
14711
14882
  try {
14712
- return JSON.parse(await readFile6(path7.join(stateDir(dir), name), "utf8"));
14883
+ return JSON.parse(await readFile6(cacheFile(dir), "utf8"));
14713
14884
  } catch (error) {
14714
14885
  if (error.code === "ENOENT") {
14715
14886
  return void 0;
@@ -14717,112 +14888,271 @@ async function readStateFile(dir, name) {
14717
14888
  throw error;
14718
14889
  }
14719
14890
  }
14720
- async function writeStateFile(dir, name, value) {
14721
- await mkdir5(stateDir(dir), { recursive: true });
14722
- const file = path7.join(stateDir(dir), name);
14723
- await writeFile6(file, JSON.stringify(value, null, 2) + "\n");
14724
- return file;
14891
+ async function writeSchemaCache(dir, database, tables) {
14892
+ const cache = { database, tables, refreshedAt: (/* @__PURE__ */ new Date()).toISOString() };
14893
+ await mkdir5(path8.dirname(cacheFile(dir)), { recursive: true });
14894
+ await writeFile6(cacheFile(dir), JSON.stringify(cache, null, 2) + "\n");
14895
+ await pruneViewsCache(dir, new Set(tables.map((table) => table.id)));
14896
+ return cache;
14725
14897
  }
14726
-
14727
- // src/commands/list.ts
14728
- function toRow(workflow, workspace) {
14729
- return {
14730
- id: workflow.id,
14731
- title: workflow.title,
14732
- workspaceId: workspace.id,
14733
- workspaceName: workspace.name,
14734
- live: workflow.enabledVersion ?? null,
14735
- version: workflow.version,
14736
- draft: workflow.draft,
14737
- updatedAt: workflow.updatedAt ?? null,
14738
- triggers: (workflow.triggerSummaries ?? []).map(({ id, type }) => ({ id, type }))
14739
- };
14898
+ function viewsDir(dir) {
14899
+ return path8.join(dir, DB_CACHE_DIR_NAME, "views");
14740
14900
  }
14741
- async function collectWorkflows(client, { workspaceId, applicationId } = {}) {
14742
- const workspaces = await listWorkspaces(client);
14743
- const selected = workspaceId ? workspaces.filter((workspace) => workspace.id === workspaceId) : workspaces;
14744
- if (workspaceId && !selected.length) {
14901
+ async function writeViewsCache(dir, tableId, views) {
14902
+ const value = { tableId, views, refreshedAt: (/* @__PURE__ */ new Date()).toISOString() };
14903
+ await mkdir5(viewsDir(dir), { recursive: true });
14904
+ await writeFile6(path8.join(viewsDir(dir), `${tableId}.json`), JSON.stringify(value, null, 2) + "\n");
14905
+ }
14906
+ async function pruneViewsCache(dir, tableIds) {
14907
+ let names;
14908
+ try {
14909
+ names = await readdir2(viewsDir(dir));
14910
+ } catch (error) {
14911
+ if (error.code === "ENOENT") {
14912
+ return;
14913
+ }
14914
+ throw error;
14915
+ }
14916
+ await Promise.all(
14917
+ names.filter((name) => name.endsWith(".json") && !tableIds.has(name.slice(0, -".json".length))).map((name) => rm(path8.join(viewsDir(dir), name), { force: true }))
14918
+ );
14919
+ }
14920
+ async function refreshSchemaCache(client, dir, databaseId) {
14921
+ const [database, tables] = await Promise.all([getDatabase(client, databaseId), listTables(client, databaseId)]);
14922
+ return writeSchemaCache(dir, database, tables);
14923
+ }
14924
+ async function removeSchemaCache(dir) {
14925
+ await rm(path8.join(dir, DB_CACHE_DIR_NAME), { recursive: true, force: true });
14926
+ }
14927
+ async function resolveDatabase(cwd, databaseId, explicitDir) {
14928
+ const dir = path8.resolve(cwd, explicitDir ?? ".");
14929
+ const config = await readDbConfig(dir);
14930
+ if (explicitDir && !config) {
14931
+ throw new CliError(`No ${DB_CONFIG_FILE_NAME} in ${dir}.`, 'Run "softr db get <database>" to create one.');
14932
+ }
14933
+ const given = databaseId ?? (process.env[DATABASE_ID_ENV_VAR] || void 0);
14934
+ if (given) {
14935
+ return config?.databaseId === given ? { databaseId: given, dir } : { databaseId: given };
14936
+ }
14937
+ if (config) {
14938
+ return { databaseId: config.databaseId, dir };
14939
+ }
14940
+ const candidates = await listSubdirsWith(cwd, dbConfigExists);
14941
+ throw new CliError(
14942
+ `Which database? There is no ${DB_CONFIG_FILE_NAME} in ${cwd}.`,
14943
+ candidates.length ? `Databases found in: ${candidates.map((candidate) => path8.relative(cwd, candidate)).join(", ")}. Pass --dir <name> or cd into one.` : `Pass the database id, set ${DATABASE_ID_ENV_VAR}, or run "softr db get <database>".`
14944
+ );
14945
+ }
14946
+ function belongsToAnother(dir, existingId, wantedId, hint) {
14947
+ return new CliError(
14948
+ `${path8.join(dir, DB_CONFIG_FILE_NAME)} belongs to database ${existingId}${wantedId ? `, not ${wantedId}` : ""}.`,
14949
+ hint
14950
+ );
14951
+ }
14952
+ async function databaseDirFor(cwd, explicitDir, databaseId, dirName) {
14953
+ const here = path8.resolve(cwd, explicitDir ?? ".");
14954
+ const inHere = await readDbConfig(here);
14955
+ if (inHere) {
14956
+ if (databaseId && inHere.databaseId === databaseId) {
14957
+ return { dir: here, existing: true };
14958
+ }
14959
+ throw belongsToAnother(
14960
+ here,
14961
+ inHere.databaseId,
14962
+ databaseId,
14963
+ explicitDir ? "Pick another --dir." : "Run it from the parent directory (a new subdirectory is created) or pass --dir."
14964
+ );
14965
+ }
14966
+ if (explicitDir) {
14967
+ return { dir: here, existing: false };
14968
+ }
14969
+ const sibling2 = path8.join(cwd, dirName);
14970
+ const inSibling = await readDbConfig(sibling2);
14971
+ if (inSibling) {
14972
+ if (inSibling.databaseId === databaseId) {
14973
+ return { dir: sibling2, existing: true };
14974
+ }
14975
+ throw belongsToAnother(sibling2, inSibling.databaseId, databaseId, "Pass --dir <path>.");
14976
+ }
14977
+ return { dir: sibling2, existing: false };
14978
+ }
14979
+ async function findReachableDatabase(client, reference) {
14980
+ const database = byIdOrName(
14981
+ await listDatabases(client),
14982
+ reference,
14983
+ "database",
14984
+ (item) => `${item.id} (workspace ${item.workspaceId})`
14985
+ );
14986
+ if (!database) {
14745
14987
  throw new CliError(
14746
- `You have no access to workspace ${workspaceId}.`,
14747
- `Known workspaces: ${workspaces.map((w) => `${w.name} (${w.id})`).join(", ")}`
14988
+ `The token reaches no database with the id or name "${reference}".`,
14989
+ '"softr db list" shows the ones it reaches.'
14748
14990
  );
14749
14991
  }
14750
- const listing = { workspaces: [], workflows: [], failures: [] };
14751
- for (const workspace of selected) {
14752
- try {
14753
- const page = await listWorkflows(client, workspace.id, { applicationId });
14754
- listing.workspaces.push({ id: workspace.id, name: workspace.name });
14755
- listing.workflows.push(...page.items.map((workflow) => toRow(workflow, workspace)));
14756
- } catch (error) {
14757
- if (!(error instanceof SoftrApiError) || selected.length === 1) {
14758
- throw error;
14759
- }
14760
- listing.failures.push({
14761
- workspaceId: workspace.id,
14762
- workspaceName: workspace.name,
14763
- status: error.status,
14764
- message: error.body
14765
- });
14992
+ return database;
14993
+ }
14994
+ async function bindDatabaseDir(client, cwd, reference, explicitDir) {
14995
+ const here = path8.resolve(cwd, explicitDir ?? ".");
14996
+ const config = await readDbConfig(here);
14997
+ if (!reference || reference === config?.databaseId) {
14998
+ if (!config) {
14999
+ const candidates = await listSubdirsWith(cwd, dbConfigExists);
15000
+ throw new CliError(
15001
+ `Which database? There is no ${DB_CONFIG_FILE_NAME} in ${here}.`,
15002
+ candidates.length ? `Databases found in: ${candidates.map((candidate) => path8.relative(cwd, candidate)).join(", ")}. Pass --dir <name> or cd into one.` : 'Pass its id or name: softr db get <database>. "softr db list" shows the ones the token reaches.'
15003
+ );
14766
15004
  }
15005
+ return { databaseId: config.databaseId, dir: here, wrote: false };
14767
15006
  }
14768
- return listing;
15007
+ const database = await findReachableDatabase(client, reference);
15008
+ const { dir, existing } = await databaseDirFor(cwd, explicitDir, database.id, slug(database.name, "database"));
15009
+ if (!existing) {
15010
+ await writeDbConfig(dir, { databaseId: database.id });
15011
+ }
15012
+ return { databaseId: database.id, dir, wrote: !existing };
14769
15013
  }
14770
- var TITLE_WIDTH = 34;
14771
- function workflowTable(rows) {
14772
- const table = new import_cli_table3.default({
14773
- head: ["ID", "Title", "Triggers", "Status", "Version", "Draft"],
14774
- // Only the free-text title wraps; the other columns size to their content, so trigger types are never cut off.
14775
- colWidths: [null, TITLE_WIDTH, null, null, null, null],
14776
- wordWrap: true,
14777
- style: { head: [], border: [], "padding-left": 1, "padding-right": 1 }
14778
- });
14779
- for (const row of rows) {
14780
- table.push([
14781
- row.id,
14782
- row.title,
14783
- row.triggers.map((trigger) => trigger.type).join("\n") || "-",
14784
- row.live === null ? "not enabled" : `live v${row.live}`,
14785
- row.version,
14786
- row.draft
14787
- ]);
15014
+
15015
+ // src/commands/db/help.ts
15016
+ var DATABASE_ID_HELP = `the database id (default: $SOFTR_DATABASE_ID, else the one in ${DB_CONFIG_FILE_NAME})`;
15017
+ var DATABASE_DIR_HELP = `database directory, the one holding ${DB_CONFIG_FILE_NAME} (default: the current directory)`;
15018
+
15019
+ // src/api/studio.ts
15020
+ async function resolveTokenOwner(client) {
15021
+ try {
15022
+ return await studioRequest(client, "POST", "/v1/users/me/token", { token: client.token });
15023
+ } catch (error) {
15024
+ if (error instanceof SoftrApiError && error.status === 404) {
15025
+ throw new CliError(
15026
+ "The studio does not know this token: revoked, expired, or from another environment.",
15027
+ 'Create a new personal API token in the studio, or run "softr login" for the browser login.'
15028
+ );
15029
+ }
15030
+ throw error;
14788
15031
  }
14789
- return table.toString();
14790
15032
  }
14791
- function renderWorkflows({ workspaces, workflows: workflows2, failures }) {
14792
- const blocks = [];
14793
- for (const workspace of workspaces) {
14794
- const rows = workflows2.filter((row) => row.workspaceId === workspace.id);
14795
- const heading = `Workspace: ${workspace.name} (${workspace.id})`;
14796
- blocks.push(rows.length ? `${heading}
14797
- ${workflowTable(rows)}` : `${heading}
14798
- (no workflows)`);
15033
+ var WORKFLOWS_READ_SCOPE = "workflows:read";
15034
+ var DATABASES_SCHEMA_WRITE_SCOPE = "databases.schema:write";
15035
+ function grantsActionOf(statements, prefix) {
15036
+ return (statements ?? []).some(
15037
+ (statement) => statement.actions.some((action) => action === "*" || action.startsWith(prefix))
15038
+ );
15039
+ }
15040
+ function grantsWorkflows(statements) {
15041
+ return grantsActionOf(statements, "workflows:");
15042
+ }
15043
+ function grantsDatabases(statements) {
15044
+ return grantsActionOf(statements, "databases.");
15045
+ }
15046
+ async function listWorkspacesWith(client, scope) {
15047
+ const reachable = await listWorkspaces(client);
15048
+ return { reachable, granted: reachable.filter((workspace) => !workspace.scopes || workspace.scopes.includes(scope)) };
15049
+ }
15050
+ async function listWorkspaces(client) {
15051
+ const workspaces = await studioRequest(client, "GET", "/v1/api/me/workspaces");
15052
+ return workspaces.map((workspace) => ({
15053
+ id: workspace.id,
15054
+ name: workspace.name,
15055
+ ...workspace.role ? { userRole: workspace.role } : {},
15056
+ isDefault: workspace.isDefault,
15057
+ ...workspace.scopes ? { scopes: workspace.scopes } : {}
15058
+ }));
15059
+ }
15060
+ function listWorkspaceIntegrations(client, workspaceId) {
15061
+ return studioRequest(client, "GET", `/v1/api/me/workspaces/${workspaceId}/integrations`);
15062
+ }
15063
+
15064
+ // src/commands/init.ts
15065
+ import path10 from "node:path";
15066
+
15067
+ // src/api/workflows.ts
15068
+ function getWorkflow(client, workflowId) {
15069
+ return apiRequest(client, "GET", `/v1/workflows/${workflowId}`);
15070
+ }
15071
+ function updateWorkflow(client, workflowId, body) {
15072
+ return apiRequest(client, "PUT", `/v1/workflows/${workflowId}`, body);
15073
+ }
15074
+ function getWorkflowOutputs(client, workflowId) {
15075
+ return apiRequest(client, "GET", `/v1/workflows/${workflowId}/outputs`);
15076
+ }
15077
+ function listWorkflows(client, workspaceId, { applicationId } = {}) {
15078
+ const query = new URLSearchParams({ workspaceId, ...applicationId ? { applicationId } : {}, limit: "500" });
15079
+ return apiRequest(client, "GET", `/v1/workflows?${query}`);
15080
+ }
15081
+ function pinWorkflow(client, workflowId, applicationId) {
15082
+ return apiRequestVoid(client, "POST", `/v1/workflows/${workflowId}/pinned-applications/${applicationId}`);
15083
+ }
15084
+ function unpinWorkflow(client, workflowId, applicationId) {
15085
+ return apiRequestVoid(client, "DELETE", `/v1/workflows/${workflowId}/pinned-applications/${applicationId}`);
15086
+ }
15087
+ function testAction(client, workflowId, hierarchyPath2, mode) {
15088
+ return apiRequest(client, "POST", `/v1/workflows/${workflowId}/outputs/${mode}/actions/${hierarchyPath2}`, {});
15089
+ }
15090
+ function testTrigger(client, workflowId, triggerId) {
15091
+ return apiRequest(client, "POST", `/v1/workflows/${workflowId}/outputs/triggers/${triggerId}/test`, {});
15092
+ }
15093
+ function saveTriggerSample(client, workflowId, triggerId, sample) {
15094
+ return apiRequestVoid(client, "POST", `/v1/workflows/${workflowId}/sample-output/triggers/${triggerId}`, sample);
15095
+ }
15096
+ function createWorkflow(client, body) {
15097
+ return apiRequest(client, "POST", "/v1/workflows", body);
15098
+ }
15099
+ function enableWorkflow(client, workflowId) {
15100
+ return apiRequestVoid(client, "PUT", `/v1/workflows/${workflowId}/enabled`);
15101
+ }
15102
+ function disableWorkflow(client, workflowId) {
15103
+ return apiRequestVoid(client, "DELETE", `/v1/workflows/${workflowId}/enabled`);
15104
+ }
15105
+ async function getEnabledVersion(client, workflowId) {
15106
+ try {
15107
+ return await apiRequest(client, "GET", `/v1/workflows/${workflowId}/enabled`);
15108
+ } catch (error) {
15109
+ if (error instanceof SoftrApiError && error.status === 404) {
15110
+ return null;
15111
+ }
15112
+ throw error;
14799
15113
  }
14800
- if (failures.length) {
14801
- blocks.push(
14802
- [
14803
- `Listing failed for ${failures.length} workspace${failures.length === 1 ? "" : "s"} (skipped):`,
14804
- ...failures.map(
14805
- (failure) => ` ${failure.workspaceName} (${failure.workspaceId}): status ${failure.status}${failure.message ? `: ${failure.message}` : ""}`
14806
- )
14807
- ].join("\n")
14808
- );
15114
+ }
15115
+ function getWorkflowReadiness(client, workflowId) {
15116
+ return apiRequest(client, "GET", `/v1/workflows/${workflowId}/readiness`);
15117
+ }
15118
+
15119
+ // src/project/state.ts
15120
+ import { mkdir as mkdir6, readFile as readFile7, writeFile as writeFile7 } from "node:fs/promises";
15121
+ import path9 from "node:path";
15122
+ var STATE_DIR_NAME = ".softr-workflows";
15123
+ function stateDir(dir) {
15124
+ return path9.join(dir, STATE_DIR_NAME);
15125
+ }
15126
+ async function readState(dir) {
15127
+ try {
15128
+ return JSON.parse(await readFile7(path9.join(stateDir(dir), "state.json"), "utf8"));
15129
+ } catch (error) {
15130
+ if (error.code === "ENOENT") {
15131
+ return void 0;
15132
+ }
15133
+ throw error;
14809
15134
  }
14810
- return blocks.join("\n\n");
14811
15135
  }
14812
- async function emitWorkflowList(options, client, { workspaceId, applicationId, human } = {}) {
14813
- const listing = await collectWorkflows(client, { workspaceId, applicationId });
14814
- emit(options, [human, renderWorkflows(listing)].filter(Boolean).join("\n\n"), { ...listing });
15136
+ async function writeState(dir, state) {
15137
+ await mkdir6(stateDir(dir), { recursive: true });
15138
+ const value = { ...state, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
15139
+ await writeFile7(path9.join(stateDir(dir), "state.json"), JSON.stringify(value, null, 2) + "\n");
14815
15140
  }
14816
- function registerList(program3) {
14817
- 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) => {
14818
- const options = globalOptions(command);
14819
- const target = resolveTarget(await readState(process.cwd()));
14820
- const client = await authenticatedClient(target);
14821
- await emitWorkflowList(options, client, {
14822
- workspaceId: opts.workspace ?? process.env["SOFTR_WORKSPACE_ID"],
14823
- applicationId: opts.app
14824
- });
14825
- });
15141
+ async function readStateFile(dir, name) {
15142
+ try {
15143
+ return JSON.parse(await readFile7(path9.join(stateDir(dir), name), "utf8"));
15144
+ } catch (error) {
15145
+ if (error.code === "ENOENT") {
15146
+ return void 0;
15147
+ }
15148
+ throw error;
15149
+ }
15150
+ }
15151
+ async function writeStateFile(dir, name, value) {
15152
+ await mkdir6(stateDir(dir), { recursive: true });
15153
+ const file = path9.join(stateDir(dir), name);
15154
+ await writeFile7(file, JSON.stringify(value, null, 2) + "\n");
15155
+ return file;
14826
15156
  }
14827
15157
 
14828
15158
  // src/catalog/schema.ts
@@ -15056,302 +15386,562 @@ function registerSchema(program3) {
15056
15386
  });
15057
15387
  }
15058
15388
 
15059
- // src/commands/pull.ts
15060
- function slug(title) {
15061
- return title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "workflow";
15062
- }
15063
- async function targetDir(cwd, explicitDir, workflowId, title) {
15064
- if (explicitDir) {
15065
- return path8.resolve(cwd, explicitDir);
15389
+ // src/commands/init.ts
15390
+ var TRIGGER_ID = "trigger";
15391
+ function requireGrantedWorkspace({ reachable, granted }, requested, scope) {
15392
+ if (granted.some((workspace) => workspace.id === requested)) {
15393
+ return;
15066
15394
  }
15067
- if (await configExists(cwd)) {
15068
- const existing = await readConfig(cwd);
15069
- if (existing.id && existing.id !== workflowId) {
15070
- throw new CliError(
15071
- `${CONFIG_FILE_NAME} here belongs to workflow ${existing.id}, not ${workflowId}.`,
15072
- "Run pull from the parent directory (a new subdirectory is created) or pass --dir."
15073
- );
15074
- }
15075
- return cwd;
15395
+ if (reachable.some((workspace) => workspace.id === requested)) {
15396
+ throw new CliError(
15397
+ `The token does not grant ${scope} on workspace ${requested}.`,
15398
+ `Check the token's grants with "softr whoami", or run "softr login" again.`
15399
+ );
15076
15400
  }
15077
- return path8.join(cwd, slug(title));
15401
+ throw new CliError(
15402
+ `You have no access to workspace ${requested}.`,
15403
+ `Known workspaces: ${granted.map((w) => `${w.name} (${w.id})`).join(", ")}`
15404
+ );
15078
15405
  }
15079
- function registerPull(program3) {
15080
- program3.command("pull").description("Download a workflow into softr-workflows.jsonc and one code file per custom code node").argument("[workflowId]", "the workflow id (last segment of its studio URL); omit to list what you can pull").option("--dir <path>", "target directory (default: this directory if it holds the workflow, else ./<title-slug>)").option("--force", "overwrite an existing softr-workflows.jsonc and its code files").action(async (workflowId, opts, command) => {
15081
- const options = globalOptions(command);
15082
- const cwd = process.cwd();
15083
- const remembered = await readState(opts.dir ? path8.resolve(cwd, opts.dir) : cwd);
15084
- const target = resolveTarget(remembered);
15085
- const client = await authenticatedClient(target);
15086
- if (!workflowId) {
15087
- await emitWorkflowList(options, client, {
15088
- human: "Pick a workflow and run: softr workflows pull <workflowId>"
15089
- });
15090
- return;
15091
- }
15092
- const workflow = await getWorkflow(client, workflowId);
15093
- const dir = await targetDir(cwd, opts.dir, workflowId, workflow.title);
15094
- if (await configExists(dir) && !opts.force) {
15095
- throw new CliError(
15096
- `${path8.join(dir, CONFIG_FILE_NAME)} already exists.`,
15097
- "Pass --force to overwrite it and its code files."
15098
- );
15099
- }
15100
- let rows = [];
15101
- let outputsNote = "";
15102
- let outputsFile;
15103
- try {
15104
- rows = await getWorkflowOutputs(client, workflowId);
15105
- } catch (error) {
15106
- outputsNote = `
15107
- Sample outputs not available, inputData types are unknown: ${error.message}`;
15108
- }
15109
- const config = { $schema: SCHEMA_POINTER, ...toLocalConfig(workflow) };
15110
- const codeFiles = await externalizeCode(config, dir, indexOutputs(rows));
15111
- await writeSchemaFile(dir);
15112
- const configFile = await writeConfig(dir, config);
15113
- const syncedHash = definitionHash((await packWorkflow(config, dir)).packed);
15114
- await writeState(dir, { ...target, workflowId, version: workflow.version, draft: workflow.draft, syncedHash });
15115
- if (!outputsNote) {
15116
- outputsFile = await writeStateFile(dir, "outputs.json", rows);
15117
- }
15118
- const files = [configFile, ...codeFiles].map((file) => path8.relative(cwd, file));
15119
- emitWithVersionNote(
15120
- options,
15121
- config,
15122
- `Pulled "${workflow.title}" (version ${workflow.version}, draft ${workflow.draft}, ${workflow.enabled ? "enabled" : "not enabled"}).
15123
- ` + files.map((file) => ` ${file}`).join("\n") + outputsNote,
15124
- {
15125
- workflowId,
15126
- apiUrl: target.apiUrl,
15127
- title: workflow.title,
15128
- version: workflow.version,
15129
- draft: workflow.draft,
15130
- enabled: workflow.enabled ?? false,
15131
- dir,
15132
- files,
15133
- outputsFile: outputsFile ?? null
15134
- }
15135
- );
15136
- });
15137
- }
15138
-
15139
- // src/commands/add.ts
15140
- function uniqueId(base, taken) {
15141
- if (!taken.has(base)) {
15142
- return base;
15406
+ async function resolveWorkspace(client, requested, scope = WORKFLOWS_READ_SCOPE) {
15407
+ const listing = await listWorkspacesWith(client, scope);
15408
+ const workspaces = listing.granted;
15409
+ if (requested) {
15410
+ requireGrantedWorkspace(listing, requested, scope);
15411
+ return requested;
15143
15412
  }
15144
- for (let n = 2; ; n++) {
15145
- if (!taken.has(`${base}-${n}`)) {
15146
- return `${base}-${n}`;
15147
- }
15413
+ if (workspaces.length === 1 && workspaces[0]) {
15414
+ return workspaces[0].id;
15148
15415
  }
15149
- }
15150
- function defaultAfter(config, loopId) {
15151
- const order = executionOrder(config);
15152
- const container = loopId ? order.filter((node) => node.loopId === loopId) : order.filter((node) => node.loopId === void 0 && node.reachable);
15153
- if (!container.length) {
15154
- if (loopId) {
15155
- return loopId;
15156
- }
15157
- const trigger = config.triggers?.[0];
15158
- if (!trigger) {
15159
- throw new CliError("The workflow has no trigger to attach the first action to.");
15160
- }
15161
- return trigger.id;
15416
+ if (!workspaces.length) {
15417
+ throw new CliError(
15418
+ `The token grants ${scope} on no workspace.`,
15419
+ `Check the token's grants with "softr whoami", or run "softr login" again.`
15420
+ );
15162
15421
  }
15163
- const paths = loopId ? [...allActions(config.actions)].find((node) => node.id === loopId)?.inputs?.paths ?? [] : config.paths ?? [];
15164
- const withOutgoing = new Set(paths.map((p) => p.fromActionId));
15165
- const leaves = container.filter((node) => !withOutgoing.has(node.id));
15166
- if (leaves.length === 1 && leaves[0]) {
15167
- return leaves[0].id;
15422
+ const preferred = workspaces.find((workspace) => workspace.isDefault);
15423
+ if (preferred) {
15424
+ return preferred.id;
15168
15425
  }
15169
15426
  throw new CliError(
15170
- leaves.length ? `Several nodes have no successor: ${leaves.map((node) => node.id).join(", ")}.` : "Every node already has a successor.",
15171
- "Pass --after <nodeId> to say where the new node goes."
15427
+ "Pass --workspace <id>: you have access to several workspaces.",
15428
+ `Choose one of: ${workspaces.map((w) => `${w.name} (${w.id})`).join(", ")}`
15172
15429
  );
15173
15430
  }
15174
- function registerAdd(program3) {
15175
- program3.command("add").description("Add an action after an existing node, or a trigger; custom code gets its own file under actions/").argument("<type>", 'node type from "spec", e.g. CUSTOM_CODE or WEBHOOK').option("--id <id>", "node id, readable and unique (default: derived from the type)").option("--title <title>", "node title shown in the studio (default: the type label)").option("--after <nodeId>", "actions: the node the new one follows (default: the only node without a successor)").option("--to <nodeId>", "triggers: an action the new trigger leads to").option("--in-loop <loopId>", "put the node inside this loop body").option("--lang <language>", "for CUSTOM_CODE: JAVASCRIPT or PYTHON (default JAVASCRIPT)").option("--dir <path>", "workflow directory (default: the current directory)").action(async (type, opts, command) => {
15176
- const options = globalOptions(command);
15177
- const dir = await findWorkflowDir(process.cwd(), opts.dir);
15178
- const config = await readConfig(dir);
15179
- const entry = findCatalogEntry(type);
15180
- const nodes = [...config.triggers ?? [], ...allActions(config.actions)];
15181
- const taken = new Set(nodes.map((node2) => node2.id));
15182
- const id = opts.id ?? uniqueId(entry.kind === "trigger" ? "trigger" : slug(entry.label), taken);
15183
- if (taken.has(id)) {
15184
- throw new CliError(`Node id "${id}" is already used.`);
15185
- }
15186
- if (id.includes(":::") || /[\s/?#]/.test(id)) {
15187
- throw new CliError(`Node id "${id}" must not contain ":::", spaces, "/", "?" or "#".`);
15188
- }
15189
- const title = opts.title ?? entry.label;
15190
- const { inputs, codeFile } = await newNodeInputs(entry, id, title, opts.lang, dir);
15191
- const node = { id, title, type: entry.type, version: entry.version, inputs };
15192
- if (entry.kind === "trigger") {
15193
- if (opts.after || opts.inLoop) {
15194
- throw new CliError(
15195
- "Triggers start the workflow: use --to <actionId> to connect one, not --after or --in-loop."
15196
- );
15197
- }
15198
- if (opts.to && !taken.has(opts.to)) {
15199
- throw new CliError(`No node "${opts.to}" to connect to.`, `Known nodes: ${[...taken].join(", ")}`);
15431
+ function registerInit(program3) {
15432
+ 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(
15433
+ "--workspace <id>",
15434
+ "workspace to create it in (default: $SOFTR_WORKSPACE_ID, else your only or default workspace)"
15435
+ ).option("--pin-to-app <id>", "application to pin the workflow to (default: $SOFTR_PINNED_APP_ID when set)").option("--trigger <TYPE>", 'trigger type from "spec" (default WEBHOOK)', "WEBHOOK").option("--dir <path>", "directory to create (default: ./<title-slug>)").action(
15436
+ async (title, opts, command) => {
15437
+ const options = globalOptions(command);
15438
+ const cwd = process.cwd();
15439
+ const dir = path10.resolve(cwd, opts.dir ?? slug(title));
15440
+ if (await configExists(dir)) {
15441
+ throw new CliError(`${path10.join(dir, CONFIG_FILE_NAME)} already exists.`, "Pick another --dir or title.");
15200
15442
  }
15201
- await editConfig(dir, [
15202
- { path: ["triggers", -1], value: node },
15203
- ...opts.to ? [{ path: ["paths", -1], value: { fromActionId: id, toActionId: opts.to } }] : []
15204
- ]);
15205
- const notes = [
15206
- (config.triggers?.length ?? 0) >= 1 ? "The service accepts one trigger per published workflow for now; multi-trigger support is coming." : "",
15207
- opts.to ? "" : `Connect it with a path to the first action (or pass --to next time).`,
15208
- `Then: softr workflows push, softr workflows test ${id}.`
15209
- ].filter(Boolean);
15210
- emitWithVersionNote(
15443
+ const trigger = findCatalogEntry(opts.trigger, "trigger");
15444
+ const target = resolveTarget();
15445
+ const client = await authenticatedClient(target);
15446
+ const workspaceId = await resolveWorkspace(client, opts.workspace ?? process.env["SOFTR_WORKSPACE_ID"]);
15447
+ const pinToApplicationId = opts.pinToApp ?? process.env["SOFTR_PINNED_APP_ID"];
15448
+ const created = await createWorkflow(client, {
15449
+ workspaceId,
15450
+ applicationId: null,
15451
+ ...pinToApplicationId ? { pinToApplicationId } : {},
15452
+ title,
15453
+ thumbnail: null,
15454
+ triggers: [{ id: TRIGGER_ID, type: trigger.type, version: trigger.version, inputs: trigger.template }],
15455
+ actions: [],
15456
+ paths: [],
15457
+ // Like the studio: the author's time zone, so schedules and waits mean what they read.
15458
+ configuration: { timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC" }
15459
+ });
15460
+ const workflow = await getWorkflow(client, created.id);
15461
+ const config = { $schema: SCHEMA_POINTER, ...toLocalConfig(workflow) };
15462
+ const configFile2 = await writeConfig(dir, config);
15463
+ await writeSchemaFile(dir);
15464
+ const syncedHash = definitionHash((await packWorkflow(config, dir)).packed);
15465
+ await writeState(dir, {
15466
+ ...target,
15467
+ workflowId: created.id,
15468
+ version: workflow.version,
15469
+ draft: workflow.draft,
15470
+ syncedHash
15471
+ });
15472
+ const studioUrl = `${target.studioUrl}/workflow/${created.id}`;
15473
+ emit(
15211
15474
  options,
15212
- config,
15213
- `Added ${entry.type} trigger "${id}"${opts.to ? ` leading to "${opts.to}"` : ""}.
15214
- ${notes.join("\n")}`,
15475
+ `Created "${title}" (${created.id}) with a ${trigger.type} trigger "${TRIGGER_ID}" in workspace ${workspaceId}${pinToApplicationId ? `, linked to application ${pinToApplicationId}` : ""}.
15476
+ ${path10.relative(cwd, configFile2)}
15477
+ ${studioUrl}
15478
+ Next: softr workflows add <TYPE> --dir ${path10.relative(cwd, dir) || "."} (see "spec" for the types), then push and test.`,
15215
15479
  {
15216
- id,
15217
- kind: "trigger",
15218
- type: entry.type,
15219
- to: opts.to ?? null
15480
+ workflowId: created.id,
15481
+ workspaceId,
15482
+ pinnedToApplicationId: pinToApplicationId ?? null,
15483
+ title,
15484
+ triggerId: TRIGGER_ID,
15485
+ triggerType: trigger.type,
15486
+ dir,
15487
+ studioUrl
15220
15488
  }
15221
15489
  );
15222
- return;
15223
15490
  }
15224
- if (opts.inLoop) {
15225
- const loop = [...allActions(config.actions)].find((candidate) => candidate.id === opts.inLoop);
15226
- if (!loop || loop.type !== LOOP_TYPE) {
15227
- throw new CliError(`"${opts.inLoop}" is not a loop in this workflow.`);
15491
+ );
15492
+ }
15493
+
15494
+ // src/commands/db/databases.ts
15495
+ function renderDatabases(databases) {
15496
+ if (!databases.length) {
15497
+ return "The token reaches no database. Create one with: softr db create <name>";
15498
+ }
15499
+ const table = new import_cli_table3.default(plainTable(["ID", "Name", "Workspace", "Tables"], [38, 34, 38, 8]));
15500
+ for (const database of databases) {
15501
+ table.push([database.id, database.name, database.workspaceId, String(database.tablesCount)]);
15502
+ }
15503
+ return table.toString();
15504
+ }
15505
+ function renderDatabase(database, tables) {
15506
+ return [
15507
+ `${database.name} (${database.id})`,
15508
+ ...database.description ? [` ${database.description}`] : [],
15509
+ ` Workspace: ${database.workspaceId}`,
15510
+ ` Created: ${database.createdAt}`,
15511
+ ` Updated: ${database.updatedAt}`,
15512
+ ` Tables: ${tables.length}`,
15513
+ ...tables.map((table) => ` ${table.name} (${table.id}), ${table.fields.length} fields`)
15514
+ ].join("\n");
15515
+ }
15516
+ function relativeDir(cwd, dir) {
15517
+ return path11.relative(cwd, dir) || ".";
15518
+ }
15519
+ function registerDatabases(db2) {
15520
+ db2.command("list").description("List the databases the token reaches, across all workspaces").action(async (_opts, command) => {
15521
+ const options = globalOptions(command);
15522
+ const client = await authenticatedClient(resolveTarget());
15523
+ const databases = await listDatabases(client);
15524
+ emit(options, renderDatabases(databases), { databases });
15525
+ });
15526
+ db2.command("get").description(
15527
+ `Show a database with its tables and keep its directory: ${DB_CONFIG_FILE_NAME} and the schema cache in ${DB_CACHE_DIR_NAME}/`
15528
+ ).argument(
15529
+ "[database]",
15530
+ `the database id or exact name (default: $SOFTR_DATABASE_ID, else the one in ${DB_CONFIG_FILE_NAME})`
15531
+ ).option(
15532
+ "--dir <path>",
15533
+ "database directory (default: this directory if it holds a database, else a new ./<name-slug> for the one given)"
15534
+ ).action(async (argument, opts, command) => {
15535
+ const options = globalOptions(command);
15536
+ const cwd = process.cwd();
15537
+ const reference = argument ?? (process.env[DATABASE_ID_ENV_VAR] || void 0);
15538
+ const client = await authenticatedClient(resolveTarget());
15539
+ const { databaseId, dir, wrote } = await bindDatabaseDir(client, cwd, reference, opts.dir);
15540
+ const { database, tables } = await refreshSchemaCache(client, dir, databaseId);
15541
+ const note = wrote ? `
15542
+ Wrote ${path11.join(relativeDir(cwd, dir), DB_CONFIG_FILE_NAME)}.` : "";
15543
+ emit(options, renderDatabase(database, tables) + note, { database, tables, dir });
15544
+ });
15545
+ db2.command("create").description(`Create an empty database and a directory for it with ${DB_CONFIG_FILE_NAME}`).argument("<name>", "the database name").option("--description <text>", "what the database holds").option(
15546
+ "--workspace <id>",
15547
+ "workspace to create it in (default: $SOFTR_WORKSPACE_ID, else your only or default workspace)"
15548
+ ).option("--dir <path>", "directory to create (default: ./<name-slug>)").action(
15549
+ async (name, opts, command) => {
15550
+ const options = globalOptions(command);
15551
+ const cwd = process.cwd();
15552
+ const { dir } = await databaseDirFor(cwd, opts.dir, void 0, slug(name, "database"));
15553
+ const client = await authenticatedClient(resolveTarget());
15554
+ const workspaceId = await resolveWorkspace(
15555
+ client,
15556
+ opts.workspace ?? process.env["SOFTR_WORKSPACE_ID"],
15557
+ DATABASES_SCHEMA_WRITE_SCOPE
15558
+ );
15559
+ const database = await createDatabase(client, { workspaceId, name, description: opts.description });
15560
+ await writeDbConfig(dir, { databaseId: database.id });
15561
+ await refreshSchemaCache(client, dir, database.id);
15562
+ emit(
15563
+ options,
15564
+ `Created database ${database.name} (${database.id}) in workspace ${workspaceId}.
15565
+ ${path11.join(relativeDir(cwd, dir), DB_CONFIG_FILE_NAME)}`,
15566
+ { database, dir }
15567
+ );
15568
+ }
15569
+ );
15570
+ db2.command("update").description("Change a database's name or description").argument("[databaseId]", DATABASE_ID_HELP).option("--name <name>", "the new name").option("--description <text>", "the new description").option("--dir <path>", DATABASE_DIR_HELP).action(
15571
+ async (databaseId, opts, command) => {
15572
+ if (!opts.name && !opts.description) {
15573
+ throw new CliError("Nothing to change: pass --name, --description or both.");
15228
15574
  }
15575
+ const options = globalOptions(command);
15576
+ const target = await resolveDatabase(process.cwd(), databaseId, opts.dir);
15577
+ const client = await authenticatedClient(resolveTarget());
15578
+ const database = await updateDatabase(client, target.databaseId, {
15579
+ name: opts.name,
15580
+ description: opts.description
15581
+ });
15582
+ if (target.dir) {
15583
+ const cache = await readSchemaCache(target.dir);
15584
+ await (cache ? writeSchemaCache(target.dir, database, cache.tables) : refreshSchemaCache(client, target.dir, database.id));
15585
+ }
15586
+ emit(options, `Updated database ${database.name} (${database.id}).`, { database });
15229
15587
  }
15230
- const after = opts.after ?? defaultAfter(config, opts.inLoop);
15231
- if (!taken.has(after)) {
15232
- throw new CliError(`No node "${after}" to attach to.`, `Known nodes: ${[...taken].join(", ")}`);
15588
+ );
15589
+ db2.command("delete").description("Delete a database; one that still has tables needs --force").argument("[databaseId]", DATABASE_ID_HELP).option("--force", "also delete its tables and all their records").option("--dir <path>", DATABASE_DIR_HELP).action(async (databaseId, opts, command) => {
15590
+ const options = globalOptions(command);
15591
+ const target = await resolveDatabase(process.cwd(), databaseId, opts.dir);
15592
+ const client = await authenticatedClient(resolveTarget());
15593
+ await deleteDatabase(client, target.databaseId, opts.force === true);
15594
+ if (target.dir) {
15595
+ await removeSchemaCache(target.dir);
15233
15596
  }
15234
- const loopIndex = opts.inLoop ? (config.actions ?? []).findIndex((action) => action.id === opts.inLoop) : -1;
15235
- const base = opts.inLoop && loopIndex >= 0 ? ["actions", loopIndex, "inputs"] : [];
15236
- await editConfig(dir, [
15237
- { path: [...base, "actions", -1], value: node },
15238
- { path: [...base, "paths", -1], value: { fromActionId: after, toActionId: id } }
15239
- ]);
15240
- const next = [
15241
- codeFile ? `Write the function in ${path9.relative(process.cwd(), codeFile)} and map its inputs under inputs.inputData in softr-workflows.jsonc.` : "",
15242
- entry.type === "BRANCH" || entry.type === "FILTER" ? `Add the condition(s) on the path(s) leaving it in softr-workflows.jsonc (see "spec ${entry.type}").` : "",
15243
- `Then: softr workflows push, softr workflows test ${id}.`
15244
- ].filter(Boolean);
15245
- emitWithVersionNote(
15597
+ emit(
15246
15598
  options,
15247
- config,
15248
- `Added ${entry.type} "${id}" after "${after}"${opts.inLoop ? ` inside loop "${opts.inLoop}"` : ""}.
15249
- ${next.join("\n")}`,
15250
- {
15251
- id,
15252
- kind: "action",
15253
- type: entry.type,
15254
- after,
15255
- loopId: opts.inLoop ?? null,
15256
- codeFile: codeFile ? path9.relative(process.cwd(), codeFile) : null
15257
- }
15599
+ `Deleted database ${target.databaseId}.` + (target.dir ? `
15600
+ ${path11.join(relativeDir(process.cwd(), target.dir), DB_CONFIG_FILE_NAME)} still names the deleted database; delete the directory.` : ""),
15601
+ { deleted: target.databaseId }
15258
15602
  );
15259
15603
  });
15260
15604
  }
15261
15605
 
15262
- // src/commands/init.ts
15263
- import path10 from "node:path";
15264
- var TRIGGER_ID = "trigger";
15265
- async function resolveWorkspace(client, requested) {
15266
- const workspaces = await listWorkspaces(client);
15267
- if (requested) {
15268
- if (!workspaces.some((workspace) => workspace.id === requested)) {
15269
- throw new CliError(
15270
- `You have no access to workspace ${requested}.`,
15271
- `Known workspaces: ${workspaces.map((w) => `${w.name} (${w.id})`).join(", ")}`
15272
- );
15606
+ // src/commands/db/schema.ts
15607
+ var import_cli_table32 = __toESM(require_cli_table3(), 1);
15608
+
15609
+ // src/commands/db/json-input.ts
15610
+ import { readFile as readFile8 } from "node:fs/promises";
15611
+ async function readJsonArgument(value, option, hint) {
15612
+ let text = value;
15613
+ if (value === "-") {
15614
+ const chunks = [];
15615
+ for await (const chunk of process.stdin) {
15616
+ chunks.push(chunk);
15617
+ }
15618
+ text = Buffer.concat(chunks).toString("utf8");
15619
+ } else if (value.startsWith("@")) {
15620
+ text = await readFile8(value.slice(1), "utf8");
15621
+ }
15622
+ try {
15623
+ return JSON.parse(text);
15624
+ } catch {
15625
+ throw new CliError(`${option} is not valid JSON.`, hint ?? "Pass inline JSON, @<file> or - for stdin.");
15626
+ }
15627
+ }
15628
+
15629
+ // src/commands/db/schema.ts
15630
+ var FIELD_REFERENCE_URL = "https://docs.softr.io/softr-api/softr-database-api/table-fields/add-table-field";
15631
+ var TABLE_ARGUMENT = "the table id or exact name";
15632
+ var FIELD_ARGUMENT = "the field id or exact name";
15633
+ function withDatabaseOptions(command) {
15634
+ return command.option("--database <id>", DATABASE_ID_HELP).option("--dir <path>", DATABASE_DIR_HELP);
15635
+ }
15636
+ async function schemaContext(opts) {
15637
+ const target = await resolveDatabase(process.cwd(), opts.database, opts.dir);
15638
+ const client = await authenticatedClient(resolveTarget());
15639
+ const cache = target.dir ? await readSchemaCache(target.dir) : void 0;
15640
+ return { client, databaseId: target.databaseId, dir: target.dir, cache };
15641
+ }
15642
+ async function currentTables(context, fresh) {
15643
+ if (!context.dir) {
15644
+ return listTables(context.client, context.databaseId);
15645
+ }
15646
+ if (!fresh && context.cache) {
15647
+ return context.cache.tables;
15648
+ }
15649
+ context.cache = await refreshSchemaCache(context.client, context.dir, context.databaseId);
15650
+ return context.cache.tables;
15651
+ }
15652
+ function tableIn(context, tables, reference) {
15653
+ const table = byIdOrName(tables, reference, "table");
15654
+ if (!table) {
15655
+ throw new CliError(
15656
+ `Database ${context.databaseId} has no table with the id or name "${reference}".`,
15657
+ '"softr db tables list" shows its tables.'
15658
+ );
15659
+ }
15660
+ return table;
15661
+ }
15662
+ async function findTableOnServer(context, reference) {
15663
+ return tableIn(context, await currentTables(context, true), reference);
15664
+ }
15665
+ async function findTableCached(context, reference) {
15666
+ const cached = context.dir && context.cache ? byIdOrName(context.cache.tables, reference, "table") : void 0;
15667
+ return cached ?? findTableOnServer(context, reference);
15668
+ }
15669
+ async function findField(context, tableReference, reference) {
15670
+ const table = await findTableOnServer(context, tableReference);
15671
+ const field = byIdOrName(table.fields, reference, "field");
15672
+ if (!field) {
15673
+ throw new CliError(
15674
+ `Table ${table.name} has no field with the id or name "${reference}".`,
15675
+ `"softr db tables get ${table.id}" shows its fields.`
15676
+ );
15677
+ }
15678
+ return { table, field };
15679
+ }
15680
+ async function afterSchemaWrite(context) {
15681
+ if (context.dir) {
15682
+ context.cache = await refreshSchemaCache(context.client, context.dir, context.databaseId);
15683
+ }
15684
+ }
15685
+ function parseOptions(json) {
15686
+ if (json === void 0) {
15687
+ return void 0;
15688
+ }
15689
+ try {
15690
+ return JSON.parse(json);
15691
+ } catch {
15692
+ throw new CliError("--options is not valid JSON.", `The options of each field type: ${FIELD_REFERENCE_URL}`);
15693
+ }
15694
+ }
15695
+ function parseFieldSpec(spec) {
15696
+ const colon = spec.lastIndexOf(":");
15697
+ const name = colon > 0 ? spec.slice(0, colon).trim() : "";
15698
+ const type = colon > 0 ? spec.slice(colon + 1).trim().toUpperCase() : "";
15699
+ if (!name || !type) {
15700
+ throw new CliError(`--field "${spec}" must be <name>:<TYPE>, for example "Due date:DATE".`);
15701
+ }
15702
+ return { name, type };
15703
+ }
15704
+ function parseFieldList(value) {
15705
+ if (!Array.isArray(value)) {
15706
+ throw new CliError(
15707
+ "--fields must be a JSON array of fields.",
15708
+ `For example: [{"name": "Stage", "type": "SELECT", "options": {...}}]`
15709
+ );
15710
+ }
15711
+ return value.map((item, index) => {
15712
+ const field = item;
15713
+ if (!field || typeof field !== "object" || typeof field.name !== "string" || !field.name || typeof field.type !== "string" || !field.type) {
15714
+ throw new CliError(`--fields item ${index + 1} needs a "name" and a "type".`);
15273
15715
  }
15274
- return requested;
15716
+ return { ...field, name: field.name, type: field.type.toUpperCase() };
15717
+ });
15718
+ }
15719
+ function renderTables(tables) {
15720
+ if (!tables.length) {
15721
+ return "The database has no tables. Create one with: softr db tables create <name>";
15275
15722
  }
15276
- if (workspaces.length === 1 && workspaces[0]) {
15277
- return workspaces[0].id;
15723
+ const table = new import_cli_table32.default(plainTable(["ID", "Name", "Fields", "Primary field"], [18, 34, 8, 30]));
15724
+ for (const item of tables) {
15725
+ const primary = item.fields.find((field) => field.id === item.primaryFieldId);
15726
+ table.push([item.id, item.name, String(item.fields.length), primary?.name ?? item.primaryFieldId]);
15278
15727
  }
15279
- const preferred = workspaces.find((workspace) => workspace.isDefault);
15280
- if (preferred) {
15281
- return preferred.id;
15728
+ return table.toString();
15729
+ }
15730
+ function flags(field, primaryFieldId) {
15731
+ return [
15732
+ field.id === primaryFieldId ? "primary" : "",
15733
+ field.required ? "required" : "",
15734
+ field.readonly ? "read-only" : "",
15735
+ field.allowMultipleEntries ? "multiple" : ""
15736
+ ].filter(Boolean).join(", ");
15737
+ }
15738
+ function renderTable(item) {
15739
+ const fields = new import_cli_table32.default(plainTable(["ID", "Name", "Type", ""], [18, 30, 18, 30]));
15740
+ for (const field of item.fields) {
15741
+ fields.push([field.id, field.name, field.type, flags(field, item.primaryFieldId)]);
15282
15742
  }
15283
- throw new CliError(
15284
- "Pass --workspace <id>: you have access to several workspaces.",
15285
- `Choose one of: ${workspaces.map((w) => `${w.name} (${w.id})`).join(", ")}`
15743
+ return [
15744
+ `${item.name} (${item.id})`,
15745
+ ...item.description ? [` ${item.description}`] : [],
15746
+ ` Default view: ${item.defaultViewId}`,
15747
+ fields.toString()
15748
+ ].join("\n");
15749
+ }
15750
+ function renderField(table, field) {
15751
+ return [
15752
+ `${field.name} (${field.id}) in ${table.name}`,
15753
+ ...field.description ? [` ${field.description}`] : [],
15754
+ ` Type: ${field.type}`,
15755
+ ...flags(field, table.primaryFieldId) ? [` Flags: ${flags(field, table.primaryFieldId)}`] : [],
15756
+ ` Options: ${JSON.stringify(field.options ?? null)}`
15757
+ ].join("\n");
15758
+ }
15759
+ function renderViews(views) {
15760
+ const table = new import_cli_table32.default(plainTable(["ID", "Name"], [18, 50]));
15761
+ for (const view of views) {
15762
+ table.push([view.id, view.name]);
15763
+ }
15764
+ return table.toString();
15765
+ }
15766
+ function registerTables(db2) {
15767
+ const tables = db2.command("tables").description("The tables of a database, with their fields");
15768
+ withDatabaseOptions(tables.command("list").description("List the tables of the database")).action(
15769
+ async (opts, command) => {
15770
+ const options = globalOptions(command);
15771
+ const context = await schemaContext(opts);
15772
+ const items = await currentTables(context, true);
15773
+ emit(options, renderTables(items), { databaseId: context.databaseId, tables: items });
15774
+ }
15775
+ );
15776
+ withDatabaseOptions(
15777
+ tables.command("get").description("Show a table with its fields").argument("<table>", TABLE_ARGUMENT)
15778
+ ).action(async (reference, opts, command) => {
15779
+ const options = globalOptions(command);
15780
+ const context = await schemaContext(opts);
15781
+ const table = await findTableOnServer(context, reference);
15782
+ emit(options, renderTable(table), { table });
15783
+ });
15784
+ withDatabaseOptions(
15785
+ tables.command("create").description(
15786
+ "Create a table with at least one field; the first one names a record unless --primary-field says otherwise"
15787
+ ).argument("<name>", "the table name").option("--description <text>", "what the table holds").option(
15788
+ "--field <name:TYPE>",
15789
+ "a field of a type without options, repeatable",
15790
+ (value, previous) => [...previous, value],
15791
+ []
15792
+ ).option(
15793
+ "--fields <json>",
15794
+ `fields as a JSON array of {name, type, options?, description?, allowMultipleEntries?}, @<file> or - for stdin; they follow the --field ones (types and options: ${FIELD_REFERENCE_URL})`
15795
+ ).option("--primary-field <name>", "the field that names a record (default: the first field)")
15796
+ ).action(
15797
+ async (name, opts, command) => {
15798
+ const options = globalOptions(command);
15799
+ const fields = [
15800
+ ...opts.field.map(parseFieldSpec),
15801
+ ...opts.fields === void 0 ? [] : parseFieldList(await readJsonArgument(opts.fields, "--fields"))
15802
+ ];
15803
+ if (!fields.length) {
15804
+ throw new CliError(
15805
+ 'A table needs at least one field: pass --field <name>:<TYPE>, for example --field "Name:SINGLE_LINE_TEXT", or --fields with a JSON array.',
15806
+ "The first field names a record unless --primary-field names another."
15807
+ );
15808
+ }
15809
+ if (opts.primaryField && !fields.some((field) => field.name === opts.primaryField)) {
15810
+ throw new CliError(`--primary-field "${opts.primaryField}" is not one of the fields' names.`);
15811
+ }
15812
+ const context = await schemaContext(opts);
15813
+ const table = await createTable(context.client, context.databaseId, {
15814
+ name,
15815
+ description: opts.description,
15816
+ primaryFieldName: opts.primaryField ?? fields[0].name,
15817
+ fields
15818
+ });
15819
+ await afterSchemaWrite(context);
15820
+ emit(options, `Created table ${table.name} (${table.id}) with ${table.fields.length} fields.`, { table });
15821
+ }
15822
+ );
15823
+ withDatabaseOptions(
15824
+ tables.command("update").description("Change a table's name or description").argument("<table>", TABLE_ARGUMENT).option("--name <name>", "the new name").option("--description <text>", "the new description")
15825
+ ).action(
15826
+ async (reference, opts, command) => {
15827
+ if (!opts.name && opts.description === void 0) {
15828
+ throw new CliError("Nothing to change: pass --name, --description or both.");
15829
+ }
15830
+ const options = globalOptions(command);
15831
+ const context = await schemaContext(opts);
15832
+ const current2 = await findTableOnServer(context, reference);
15833
+ const table = await updateTable(context.client, context.databaseId, current2.id, {
15834
+ name: opts.name ?? current2.name,
15835
+ description: opts.description ?? current2.description
15836
+ });
15837
+ await afterSchemaWrite(context);
15838
+ emit(options, `Updated table ${table.name} (${table.id}).`, { table });
15839
+ }
15286
15840
  );
15841
+ withDatabaseOptions(
15842
+ tables.command("delete").description("Delete a table; one that still has records needs --force").argument("<table>", TABLE_ARGUMENT).option("--force", "also delete all its records")
15843
+ ).action(async (reference, opts, command) => {
15844
+ const options = globalOptions(command);
15845
+ const context = await schemaContext(opts);
15846
+ const table = await findTableOnServer(context, reference);
15847
+ await deleteTable(context.client, context.databaseId, table.id, opts.force === true);
15848
+ await afterSchemaWrite(context);
15849
+ emit(options, `Deleted table ${table.name} (${table.id}).`, { deleted: table.id });
15850
+ });
15287
15851
  }
15288
- function registerInit(program3) {
15289
- 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(
15290
- "--workspace <id>",
15291
- "workspace to create it in (default: $SOFTR_WORKSPACE_ID, else your only or default workspace)"
15292
- ).option("--pin-to-app <id>", "application to pin the workflow to (default: $SOFTR_PINNED_APP_ID when set)").option("--trigger <TYPE>", 'trigger type from "spec" (default WEBHOOK)', "WEBHOOK").option("--dir <path>", "directory to create (default: ./<title-slug>)").action(
15293
- async (title, opts, command) => {
15852
+ function registerFields(db2) {
15853
+ const fields = db2.command("fields").description("The fields of a table");
15854
+ withDatabaseOptions(
15855
+ fields.command("get").description("Show a field with its options").argument("<table>", TABLE_ARGUMENT).argument("<field>", FIELD_ARGUMENT)
15856
+ ).action(async (tableReference, reference, opts, command) => {
15857
+ const options = globalOptions(command);
15858
+ const context = await schemaContext(opts);
15859
+ const { table, field } = await findField(context, tableReference, reference);
15860
+ emit(options, renderField(table, field), { tableId: table.id, field });
15861
+ });
15862
+ withDatabaseOptions(
15863
+ fields.command("add").description("Add a field to a table").argument("<table>", TABLE_ARGUMENT).argument("<name>", "the field name").requiredOption("--type <TYPE>", "the field type, for example SINGLE_LINE_TEXT, NUMBER, SELECT, LINKED_RECORD").option("--options <json>", `the type's options as JSON (${FIELD_REFERENCE_URL})`).option("--description <text>", "what the field holds").option("--multiple", "allow several values (SELECT, LINKED_RECORD, USER, ATTACHMENT)")
15864
+ ).action(
15865
+ async (tableReference, name, opts, command) => {
15294
15866
  const options = globalOptions(command);
15295
- const cwd = process.cwd();
15296
- const dir = path10.resolve(cwd, opts.dir ?? slug(title));
15297
- if (await configExists(dir)) {
15298
- throw new CliError(`${path10.join(dir, CONFIG_FILE_NAME)} already exists.`, "Pick another --dir or title.");
15867
+ const fieldOptions = parseOptions(opts.options);
15868
+ const context = await schemaContext(opts);
15869
+ const table = await findTableCached(context, tableReference);
15870
+ const field = await addField(context.client, context.databaseId, table.id, {
15871
+ name,
15872
+ type: opts.type.toUpperCase(),
15873
+ description: opts.description,
15874
+ ...fieldOptions === void 0 ? {} : { options: fieldOptions },
15875
+ allowMultipleEntries: opts.multiple === true
15876
+ });
15877
+ await afterSchemaWrite(context);
15878
+ emit(options, `Added field ${field.name} (${field.id}, ${field.type}) to ${table.name}.`, {
15879
+ tableId: table.id,
15880
+ field
15881
+ });
15882
+ }
15883
+ );
15884
+ withDatabaseOptions(
15885
+ fields.command("update").description("Change a field: what is not passed stays as it is").argument("<table>", TABLE_ARGUMENT).argument("<field>", FIELD_ARGUMENT).option("--name <name>", "the new name").option("--type <TYPE>", "the new type").option("--options <json>", `the new options as JSON, replacing the current ones (${FIELD_REFERENCE_URL})`).option("--description <text>", "the new description").option("--multiple", "allow several values").option("--no-multiple", "allow one value only")
15886
+ ).action(
15887
+ async (tableReference, reference, opts, command) => {
15888
+ const fieldOptions = parseOptions(opts.options);
15889
+ if (!opts.name && !opts.type && fieldOptions === void 0 && opts.description === void 0 && opts.multiple === void 0) {
15890
+ throw new CliError("Nothing to change: pass --name, --type, --options, --description or --[no-]multiple.");
15299
15891
  }
15300
- const trigger = findCatalogEntry(opts.trigger, "trigger");
15301
- const target = resolveTarget();
15302
- const client = await authenticatedClient(target);
15303
- const workspaceId = await resolveWorkspace(client, opts.workspace ?? process.env["SOFTR_WORKSPACE_ID"]);
15304
- const pinToApplicationId = opts.pinToApp ?? process.env["SOFTR_PINNED_APP_ID"];
15305
- const created = await createWorkflow(client, {
15306
- workspaceId,
15307
- applicationId: null,
15308
- ...pinToApplicationId ? { pinToApplicationId } : {},
15309
- title,
15310
- thumbnail: null,
15311
- triggers: [{ id: TRIGGER_ID, type: trigger.type, version: trigger.version, inputs: trigger.template }],
15312
- actions: [],
15313
- paths: [],
15314
- // Like the studio: the author's time zone, so schedules and waits mean what they read.
15315
- configuration: { timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC" }
15892
+ const options = globalOptions(command);
15893
+ const context = await schemaContext(opts);
15894
+ const { table, field: current2 } = await findField(context, tableReference, reference);
15895
+ const field = await updateField(context.client, context.databaseId, table.id, current2.id, {
15896
+ name: opts.name ?? current2.name,
15897
+ type: opts.type?.toUpperCase() ?? current2.type,
15898
+ description: opts.description ?? current2.description,
15899
+ options: fieldOptions ?? current2.options,
15900
+ aiOptions: current2.aiOptions ?? null,
15901
+ defaultValue: current2.defaultValue ?? null,
15902
+ allowMultipleEntries: opts.multiple ?? current2.allowMultipleEntries
15316
15903
  });
15317
- const workflow = await getWorkflow(client, created.id);
15318
- const config = { $schema: SCHEMA_POINTER, ...toLocalConfig(workflow) };
15319
- const configFile = await writeConfig(dir, config);
15320
- await writeSchemaFile(dir);
15321
- const syncedHash = definitionHash((await packWorkflow(config, dir)).packed);
15322
- await writeState(dir, {
15323
- ...target,
15324
- workflowId: created.id,
15325
- version: workflow.version,
15326
- draft: workflow.draft,
15327
- syncedHash
15904
+ await afterSchemaWrite(context);
15905
+ emit(options, `Updated field ${field.name} (${field.id}, ${field.type}) in ${table.name}.`, {
15906
+ tableId: table.id,
15907
+ field
15328
15908
  });
15329
- const studioUrl = `${target.studioUrl}/workflow/${created.id}`;
15330
- emit(
15331
- options,
15332
- `Created "${title}" (${created.id}) with a ${trigger.type} trigger "${TRIGGER_ID}" in workspace ${workspaceId}${pinToApplicationId ? `, linked to application ${pinToApplicationId}` : ""}.
15333
- ${path10.relative(cwd, configFile)}
15334
- ${studioUrl}
15335
- Next: softr workflows add <TYPE> --dir ${path10.relative(cwd, dir) || "."} (see "spec" for the types), then push and test.`,
15336
- {
15337
- workflowId: created.id,
15338
- workspaceId,
15339
- pinnedToApplicationId: pinToApplicationId ?? null,
15340
- title,
15341
- triggerId: TRIGGER_ID,
15342
- triggerType: trigger.type,
15343
- dir,
15344
- studioUrl
15345
- }
15346
- );
15347
15909
  }
15348
15910
  );
15911
+ withDatabaseOptions(
15912
+ fields.command("delete").description("Delete a field and its values in every record").argument("<table>", TABLE_ARGUMENT).argument("<field>", FIELD_ARGUMENT)
15913
+ ).action(async (tableReference, reference, opts, command) => {
15914
+ const options = globalOptions(command);
15915
+ const context = await schemaContext(opts);
15916
+ const { table, field } = await findField(context, tableReference, reference);
15917
+ await deleteField(context.client, context.databaseId, table.id, field.id);
15918
+ await afterSchemaWrite(context);
15919
+ emit(options, `Deleted field ${field.name} (${field.id}) from ${table.name}.`, {
15920
+ tableId: table.id,
15921
+ deleted: field.id
15922
+ });
15923
+ });
15924
+ }
15925
+ function registerViews(db2) {
15926
+ const views = db2.command("views").description("The views of a table");
15927
+ withDatabaseOptions(
15928
+ views.command("list").description("List the views of a table").argument("<table>", TABLE_ARGUMENT)
15929
+ ).action(async (reference, opts, command) => {
15930
+ const options = globalOptions(command);
15931
+ const context = await schemaContext(opts);
15932
+ const table = await findTableOnServer(context, reference);
15933
+ const items = await listViews(context.client, context.databaseId, table.id);
15934
+ if (context.dir) {
15935
+ await writeViewsCache(context.dir, table.id, items);
15936
+ }
15937
+ emit(options, renderViews(items), { tableId: table.id, views: items });
15938
+ });
15349
15939
  }
15350
15940
 
15351
15941
  // src/commands/integrations.ts
15352
- var import_cli_table32 = __toESM(require_cli_table3(), 1);
15353
- import path11 from "node:path";
15354
- function toRow2(integration) {
15942
+ var import_cli_table33 = __toESM(require_cli_table3(), 1);
15943
+ import path12 from "node:path";
15944
+ function toRow(integration) {
15355
15945
  return {
15356
15946
  id: integration.id,
15357
15947
  name: integration.name,
@@ -15368,7 +15958,7 @@ function renderIntegrations(rows) {
15368
15958
  if (!rows.length) {
15369
15959
  return "No integrations connected in this workspace. Connect one in the studio (Workspace settings > Integrations).";
15370
15960
  }
15371
- const table = new import_cli_table32.default({
15961
+ const table = new import_cli_table33.default({
15372
15962
  head: ["ID", "Name", "Type", "Account", "Auth"],
15373
15963
  colWidths: [38, 34, 22, 30, 11],
15374
15964
  wordWrap: true,
@@ -15382,7 +15972,7 @@ Use the ID as the integrationId input of a node (see "spec <TYPE>").`;
15382
15972
  }
15383
15973
  async function workflowDirOf(cwd, dir) {
15384
15974
  if (dir) {
15385
- return path11.resolve(cwd, dir);
15975
+ return path12.resolve(cwd, dir);
15386
15976
  }
15387
15977
  return await configExists(cwd) ? cwd : void 0;
15388
15978
  }
@@ -15399,7 +15989,7 @@ async function workspaceFor(client, requested, workflowDir) {
15399
15989
  return resolveWorkspace(client, void 0);
15400
15990
  }
15401
15991
  function registerIntegrations(program3) {
15402
- program3.command("integrations").description("List the workspace's connected integrations, whose ids go into integrationId inputs").option(
15992
+ program3.command("integrations").description("List the workspace's connected integrations").option(
15403
15993
  "--workspace <id>",
15404
15994
  "workspace to list (default: $SOFTR_WORKSPACE_ID, else the workflow directory's, else your only or default workspace)"
15405
15995
  ).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) => {
@@ -15409,7 +15999,7 @@ function registerIntegrations(program3) {
15409
15999
  const client = await authenticatedClient(target);
15410
16000
  const workspaceId = await workspaceFor(client, opts.workspace ?? process.env["SOFTR_WORKSPACE_ID"], workflowDir);
15411
16001
  const wanted = opts.type?.toUpperCase();
15412
- const rows = (await listWorkspaceIntegrations(client, workspaceId)).map(toRow2).filter((row) => !wanted || row.type === wanted).sort(byTypeThenName);
16002
+ const rows = (await listWorkspaceIntegrations(client, workspaceId)).map(toRow).filter((row) => !wanted || row.type === wanted).sort(byTypeThenName);
15413
16003
  emit(options, `Workspace ${workspaceId}${wanted ? `, type ${wanted}` : ""}:
15414
16004
  ${renderIntegrations(rows)}`, {
15415
16005
  workspaceId,
@@ -15419,6 +16009,105 @@ ${renderIntegrations(rows)}`, {
15419
16009
  });
15420
16010
  }
15421
16011
 
16012
+ // src/commands/list.ts
16013
+ var import_cli_table34 = __toESM(require_cli_table3(), 1);
16014
+ function toRow2(workflow, workspace) {
16015
+ return {
16016
+ id: workflow.id,
16017
+ title: workflow.title,
16018
+ workspaceId: workspace.id,
16019
+ workspaceName: workspace.name,
16020
+ live: workflow.enabledVersion ?? null,
16021
+ version: workflow.version,
16022
+ draft: workflow.draft,
16023
+ updatedAt: workflow.updatedAt ?? null,
16024
+ triggers: (workflow.triggerSummaries ?? []).map(({ id, type }) => ({ id, type }))
16025
+ };
16026
+ }
16027
+ async function collectWorkflows(client, { workspaceId, applicationId } = {}) {
16028
+ const workspaces = await listWorkspacesWith(client, WORKFLOWS_READ_SCOPE);
16029
+ if (workspaceId) {
16030
+ requireGrantedWorkspace(workspaces, workspaceId, WORKFLOWS_READ_SCOPE);
16031
+ }
16032
+ const selected = workspaceId ? workspaces.granted.filter((workspace) => workspace.id === workspaceId) : workspaces.granted;
16033
+ const listing = { workspaces: [], workflows: [], failures: [] };
16034
+ for (const workspace of selected) {
16035
+ try {
16036
+ const page = await listWorkflows(client, workspace.id, { applicationId });
16037
+ listing.workspaces.push({ id: workspace.id, name: workspace.name });
16038
+ listing.workflows.push(...page.items.map((workflow) => toRow2(workflow, workspace)));
16039
+ } catch (error) {
16040
+ if (!(error instanceof SoftrApiError) || selected.length === 1) {
16041
+ throw error;
16042
+ }
16043
+ listing.failures.push({
16044
+ workspaceId: workspace.id,
16045
+ workspaceName: workspace.name,
16046
+ status: error.status,
16047
+ message: error.body
16048
+ });
16049
+ }
16050
+ }
16051
+ return listing;
16052
+ }
16053
+ var TITLE_WIDTH = 34;
16054
+ function workflowTable(rows) {
16055
+ const table = new import_cli_table34.default({
16056
+ head: ["ID", "Title", "Triggers", "Status", "Version", "Draft"],
16057
+ // Only the free-text title wraps; the other columns size to their content, so trigger types are never cut off.
16058
+ colWidths: [null, TITLE_WIDTH, null, null, null, null],
16059
+ wordWrap: true,
16060
+ style: { head: [], border: [], "padding-left": 1, "padding-right": 1 }
16061
+ });
16062
+ for (const row of rows) {
16063
+ table.push([
16064
+ row.id,
16065
+ row.title,
16066
+ row.triggers.map((trigger) => trigger.type).join("\n") || "-",
16067
+ row.live === null ? "not enabled" : `live v${row.live}`,
16068
+ row.version,
16069
+ row.draft
16070
+ ]);
16071
+ }
16072
+ return table.toString();
16073
+ }
16074
+ function renderWorkflows({ workspaces, workflows: workflows2, failures }) {
16075
+ const blocks = [];
16076
+ for (const workspace of workspaces) {
16077
+ const rows = workflows2.filter((row) => row.workspaceId === workspace.id);
16078
+ const heading = `Workspace: ${workspace.name} (${workspace.id})`;
16079
+ blocks.push(rows.length ? `${heading}
16080
+ ${workflowTable(rows)}` : `${heading}
16081
+ (no workflows)`);
16082
+ }
16083
+ if (failures.length) {
16084
+ blocks.push(
16085
+ [
16086
+ `Listing failed for ${failures.length} workspace${failures.length === 1 ? "" : "s"} (skipped):`,
16087
+ ...failures.map(
16088
+ (failure) => ` ${failure.workspaceName} (${failure.workspaceId}): status ${failure.status}${failure.message ? `: ${failure.message}` : ""}`
16089
+ )
16090
+ ].join("\n")
16091
+ );
16092
+ }
16093
+ return blocks.join("\n\n");
16094
+ }
16095
+ async function emitWorkflowList(options, client, { workspaceId, applicationId, human } = {}) {
16096
+ const listing = await collectWorkflows(client, { workspaceId, applicationId });
16097
+ emit(options, [human, renderWorkflows(listing)].filter(Boolean).join("\n\n"), { ...listing });
16098
+ }
16099
+ function registerList(program3) {
16100
+ 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) => {
16101
+ const options = globalOptions(command);
16102
+ const target = resolveTarget(await readState(process.cwd()));
16103
+ const client = await authenticatedClient(target);
16104
+ await emitWorkflowList(options, client, {
16105
+ workspaceId: opts.workspace ?? process.env["SOFTR_WORKSPACE_ID"],
16106
+ applicationId: opts.app
16107
+ });
16108
+ });
16109
+ }
16110
+
15422
16111
  // src/commands/login.ts
15423
16112
  import { execFile } from "node:child_process";
15424
16113
  import { createServer } from "node:http";
@@ -15431,10 +16120,10 @@ async function identify(client) {
15431
16120
  return { userId: owner.id, email: owner.email, statements: owner.statements };
15432
16121
  }
15433
16122
  function scopeWarning(identity) {
15434
- if (grantsWorkflows(identity.statements)) {
16123
+ if (grantsWorkflows(identity.statements) || grantsDatabases(identity.statements)) {
15435
16124
  return "";
15436
16125
  }
15437
- return "\nWarning: this token grants no workflows action (workflows:read, workflows:write, workflows:execute); most commands will be refused.";
16126
+ return "\nWarning: this token grants no workflows action (workflows:read, workflows:write, workflows:execute) and no databases action (databases.records:read, databases.records:write, databases.schema:write); most commands will be refused.";
15438
16127
  }
15439
16128
  async function interactiveLogin(target) {
15440
16129
  if (!stdin.isTTY) {
@@ -15684,7 +16373,7 @@ ${identity.statements.map((s) => ` ${s.actions.join(", ")} on ${s.resources.joi
15684
16373
  }
15685
16374
 
15686
16375
  // src/commands/outputs.ts
15687
- var import_cli_table34 = __toESM(require_cli_table3(), 1);
16376
+ var import_cli_table36 = __toESM(require_cli_table3(), 1);
15688
16377
 
15689
16378
  // src/project/test-state.ts
15690
16379
  function outdatedReferences(row) {
@@ -15697,28 +16386,28 @@ function renderIssues(issues) {
15697
16386
 
15698
16387
  // src/project/variables.ts
15699
16388
  var MAX_VARIABLES = 200;
15700
- function appendKey(path13, key) {
15701
- return /^[A-Za-z_]\w*$/.test(key) ? `${path13}.${key}` : `${path13}['${key.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}']`;
16389
+ function appendKey(path15, key) {
16390
+ return /^[A-Za-z_]\w*$/.test(key) ? `${path15}.${key}` : `${path15}['${key.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}']`;
15702
16391
  }
15703
16392
  function isPrimitive2(value) {
15704
16393
  return value !== void 0 && value !== null && typeof value !== "object";
15705
16394
  }
15706
- function walk(type, raw, path13, token, out) {
16395
+ function walk(type, raw, path15, token, out) {
15707
16396
  if (out.length >= MAX_VARIABLES) {
15708
16397
  return;
15709
16398
  }
15710
- out.push({ variable: token(path13), path: path13, type, ...isPrimitive2(raw) ? { example: raw } : {} });
16399
+ out.push({ variable: token(path15), path: path15, type, ...isPrimitive2(raw) ? { example: raw } : {} });
15711
16400
  if (type.kind === "object") {
15712
16401
  for (const [key, field] of Object.entries(type.fields)) {
15713
16402
  const rawField = raw !== null && typeof raw === "object" && !Array.isArray(raw) ? raw[key] : void 0;
15714
- walk(field.type, rawField, appendKey(path13, key), token, out);
16403
+ walk(field.type, rawField, appendKey(path15, key), token, out);
15715
16404
  }
15716
16405
  } else if (type.kind === "array") {
15717
- walk(type.items, Array.isArray(raw) ? raw[0] : void 0, `${path13}[*]`, token, out);
16406
+ walk(type.items, Array.isArray(raw) ? raw[0] : void 0, `${path15}[*]`, token, out);
15718
16407
  }
15719
16408
  }
15720
16409
  function collectVariables(nodeId, sample, metadata) {
15721
- const token = (path13) => `{outputs.${nodeId}:::${path13}}`;
16410
+ const token = (path15) => `{outputs.${nodeId}:::${path15}}`;
15722
16411
  const variables = [];
15723
16412
  if (sample !== void 0 && sample !== null) {
15724
16413
  walk(typeOfJson(sample), sample, "$", token, variables);
@@ -15733,7 +16422,7 @@ function collectVariables(nodeId, sample, metadata) {
15733
16422
  }
15734
16423
 
15735
16424
  // src/commands/test.ts
15736
- var import_cli_table33 = __toESM(require_cli_table3(), 1);
16425
+ var import_cli_table35 = __toESM(require_cli_table3(), 1);
15737
16426
  var OUTPUTS_FILE = "outputs.json";
15738
16427
  function fieldCount(row) {
15739
16428
  const output = row.sampleOutput?.result?.output;
@@ -15834,7 +16523,7 @@ var LABEL = {
15834
16523
  error: "ERROR"
15835
16524
  };
15836
16525
  function renderResults(results) {
15837
- const table = new import_cli_table33.default({
16526
+ const table = new import_cli_table35.default({
15838
16527
  head: ["Result", "Node", "Type", "Detail"],
15839
16528
  colWidths: [9, 30, 24, 60],
15840
16529
  wordWrap: true,
@@ -15950,7 +16639,7 @@ function sampleState(row) {
15950
16639
  }
15951
16640
  }
15952
16641
  function renderVariables(nodeId, entries, unverified) {
15953
- const table = new import_cli_table34.default({
16642
+ const table = new import_cli_table36.default({
15954
16643
  head: ["Variable", "Type", "Example"],
15955
16644
  colWidths: [64, 32, 44],
15956
16645
  wordWrap: true,
@@ -16018,7 +16707,7 @@ function registerOutputs(program3) {
16018
16707
  outdated: Object.keys(outdatedReferences(row))
16019
16708
  };
16020
16709
  });
16021
- const table = new import_cli_table34.default({
16710
+ const table = new import_cli_table36.default({
16022
16711
  head: ["Node", "Type", "Sample", "Fields"],
16023
16712
  colWidths: [30, 24, 60, 8],
16024
16713
  wordWrap: true,
@@ -16050,19 +16739,19 @@ function pathFromKeys(keys) {
16050
16739
  const only = keys[0].trim();
16051
16740
  return only.startsWith("$") ? only : `$${only.startsWith("[") ? "" : "."}${only}`;
16052
16741
  }
16053
- let path13 = "$";
16742
+ let path15 = "$";
16054
16743
  for (const key of keys) {
16055
16744
  if (key === "*") {
16056
- path13 += "[*]";
16745
+ path15 += "[*]";
16057
16746
  } else if (/^\d+$/.test(key)) {
16058
- path13 += `[${key}]`;
16747
+ path15 += `[${key}]`;
16059
16748
  } else if (/^[A-Za-z_]\w*$/.test(key)) {
16060
- path13 += `.${key}`;
16749
+ path15 += `.${key}`;
16061
16750
  } else {
16062
- path13 += `['${key.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}']`;
16751
+ path15 += `['${key.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}']`;
16063
16752
  }
16064
16753
  }
16065
- return path13;
16754
+ return path15;
16066
16755
  }
16067
16756
  function keysAt(type) {
16068
16757
  switch (type.kind) {
@@ -16087,12 +16776,12 @@ function registerPlaceholder(program3) {
16087
16776
  }
16088
16777
  const rows = await readStateFile(dir, OUTPUTS_FILE) ?? [];
16089
16778
  const outputs = indexOutputs(rows);
16090
- const path13 = pathFromKeys(keys);
16091
- if (!parsePath(path13)) {
16092
- throw new CliError(`Cannot read the path "${path13}".`);
16779
+ const path15 = pathFromKeys(keys);
16780
+ if (!parsePath(path15)) {
16781
+ throw new CliError(`Cannot read the path "${path15}".`);
16093
16782
  }
16094
16783
  const isLoop = location.node.type === LOOP_TYPE;
16095
- const token = isLoop ? `{loopActionGroup.${nodeId}:::loopVariables.items${path13 === "$" ? "" : path13.slice(1)}}` : `{outputs.${nodeId}:::${path13}}`;
16784
+ const token = isLoop ? `{loopActionGroup.${nodeId}:::loopVariables.items${path15 === "$" ? "" : path15.slice(1)}}` : `{outputs.${nodeId}:::${path15}}`;
16096
16785
  const [description] = describeInputData(
16097
16786
  { id: "probe", type: "CUSTOM_CODE", inputs: { inputData: { probe: token } } },
16098
16787
  config,
@@ -16102,7 +16791,7 @@ function registerPlaceholder(program3) {
16102
16791
  throw new CliError("Could not evaluate the token.");
16103
16792
  }
16104
16793
  if (description.type.kind === "unknown") {
16105
- const segments = parsePath(path13) ?? [];
16794
+ const segments = parsePath(path15) ?? [];
16106
16795
  let hint = description.note ?? "not found";
16107
16796
  const row = outputs[nodeId];
16108
16797
  if (row?.hasSample && segments.length) {
@@ -16128,7 +16817,7 @@ function registerPlaceholder(program3) {
16128
16817
  emit(options, `${token}
16129
16818
  type: ${type}${example3} (${status})`, {
16130
16819
  token,
16131
- path: path13,
16820
+ path: path15,
16132
16821
  type,
16133
16822
  example: description.example ?? null,
16134
16823
  verified: description.verified,
@@ -16137,6 +16826,84 @@ type: ${type}${example3} (${status})`, {
16137
16826
  });
16138
16827
  }
16139
16828
 
16829
+ // src/commands/pull.ts
16830
+ import path13 from "node:path";
16831
+ async function targetDir(cwd, explicitDir, workflowId, title) {
16832
+ if (explicitDir) {
16833
+ return path13.resolve(cwd, explicitDir);
16834
+ }
16835
+ if (await configExists(cwd)) {
16836
+ const existing = await readConfig(cwd);
16837
+ if (existing.id && existing.id !== workflowId) {
16838
+ throw new CliError(
16839
+ `${CONFIG_FILE_NAME} here belongs to workflow ${existing.id}, not ${workflowId}.`,
16840
+ "Run pull from the parent directory (a new subdirectory is created) or pass --dir."
16841
+ );
16842
+ }
16843
+ return cwd;
16844
+ }
16845
+ return path13.join(cwd, slug(title));
16846
+ }
16847
+ function registerPull(program3) {
16848
+ program3.command("pull").description("Download a workflow into softr-workflows.jsonc and one code file per custom code node").argument("[workflowId]", "the workflow id (last segment of its studio URL); omit to list what you can pull").option("--dir <path>", "target directory (default: this directory if it holds the workflow, else ./<title-slug>)").option("--force", "overwrite an existing softr-workflows.jsonc and its code files").action(async (workflowId, opts, command) => {
16849
+ const options = globalOptions(command);
16850
+ const cwd = process.cwd();
16851
+ const remembered = await readState(opts.dir ? path13.resolve(cwd, opts.dir) : cwd);
16852
+ const target = resolveTarget(remembered);
16853
+ const client = await authenticatedClient(target);
16854
+ if (!workflowId) {
16855
+ await emitWorkflowList(options, client, {
16856
+ human: "Pick a workflow and run: softr workflows pull <workflowId>"
16857
+ });
16858
+ return;
16859
+ }
16860
+ const workflow = await getWorkflow(client, workflowId);
16861
+ const dir = await targetDir(cwd, opts.dir, workflowId, workflow.title);
16862
+ if (await configExists(dir) && !opts.force) {
16863
+ throw new CliError(
16864
+ `${path13.join(dir, CONFIG_FILE_NAME)} already exists.`,
16865
+ "Pass --force to overwrite it and its code files."
16866
+ );
16867
+ }
16868
+ let rows = [];
16869
+ let outputsNote = "";
16870
+ let outputsFile;
16871
+ try {
16872
+ rows = await getWorkflowOutputs(client, workflowId);
16873
+ } catch (error) {
16874
+ outputsNote = `
16875
+ Sample outputs not available, inputData types are unknown: ${error.message}`;
16876
+ }
16877
+ const config = { $schema: SCHEMA_POINTER, ...toLocalConfig(workflow) };
16878
+ const codeFiles = await externalizeCode(config, dir, indexOutputs(rows));
16879
+ await writeSchemaFile(dir);
16880
+ const configFile2 = await writeConfig(dir, config);
16881
+ const syncedHash = definitionHash((await packWorkflow(config, dir)).packed);
16882
+ await writeState(dir, { ...target, workflowId, version: workflow.version, draft: workflow.draft, syncedHash });
16883
+ if (!outputsNote) {
16884
+ outputsFile = await writeStateFile(dir, "outputs.json", rows);
16885
+ }
16886
+ const files = [configFile2, ...codeFiles].map((file) => path13.relative(cwd, file));
16887
+ emitWithVersionNote(
16888
+ options,
16889
+ config,
16890
+ `Pulled "${workflow.title}" (version ${workflow.version}, draft ${workflow.draft}, ${workflow.enabled ? "enabled" : "not enabled"}).
16891
+ ` + files.map((file) => ` ${file}`).join("\n") + outputsNote,
16892
+ {
16893
+ workflowId,
16894
+ apiUrl: target.apiUrl,
16895
+ title: workflow.title,
16896
+ version: workflow.version,
16897
+ draft: workflow.draft,
16898
+ enabled: workflow.enabled ?? false,
16899
+ dir,
16900
+ files,
16901
+ outputsFile: outputsFile ?? null
16902
+ }
16903
+ );
16904
+ });
16905
+ }
16906
+
16140
16907
  // src/commands/publish.ts
16141
16908
  import { stdin as stdin2, stdout } from "node:process";
16142
16909
  import { createInterface as createInterface2 } from "node:readline/promises";
@@ -16374,23 +17141,23 @@ function registerPin(program3) {
16374
17141
  }
16375
17142
 
16376
17143
  // src/commands/push.ts
16377
- import path12 from "node:path";
17144
+ import path14 from "node:path";
16378
17145
 
16379
17146
  // src/project/placeholder-check.ts
16380
17147
  var TOKEN = /\{(?:outputs|loopActionGroup)\.[^:{}]+:::[^{}]*\}/g;
16381
- function* stringsIn(value, path13) {
17148
+ function* stringsIn(value, path15) {
16382
17149
  if (typeof value === "string") {
16383
- yield [path13, value];
17150
+ yield [path15, value];
16384
17151
  } else if (Array.isArray(value)) {
16385
17152
  for (const [index, item] of value.entries()) {
16386
- yield* stringsIn(item, `${path13}[${index}]`);
17153
+ yield* stringsIn(item, `${path15}[${index}]`);
16387
17154
  }
16388
17155
  } else if (value !== null && typeof value === "object") {
16389
17156
  for (const [key, item] of Object.entries(value)) {
16390
17157
  if (key === "actions" || key === "paths") {
16391
17158
  continue;
16392
17159
  }
16393
- yield* stringsIn(item, path13 ? `${path13}.${key}` : key);
17160
+ yield* stringsIn(item, path15 ? `${path15}.${key}` : key);
16394
17161
  }
16395
17162
  }
16396
17163
  }
@@ -16501,7 +17268,7 @@ ${unbacked.map(
16501
17268
  draft: result.draft,
16502
17269
  inlined,
16503
17270
  studioUrl,
16504
- dir: path12.resolve(dir),
17271
+ dir: path14.resolve(dir),
16505
17272
  unbackedPlaceholders: unbacked
16506
17273
  }
16507
17274
  );
@@ -16509,10 +17276,10 @@ ${unbacked.map(
16509
17276
  }
16510
17277
 
16511
17278
  // src/commands/spec.ts
16512
- var import_cli_table35 = __toESM(require_cli_table3(), 1);
17279
+ var import_cli_table37 = __toESM(require_cli_table3(), 1);
16513
17280
  var TABLE_STYLE2 = { head: [], border: [], "padding-left": 1, "padding-right": 1 };
16514
17281
  function renderList() {
16515
- const table = new import_cli_table35.default({
17282
+ const table = new import_cli_table37.default({
16516
17283
  head: ["Type", "Kind", "Label", "Summary"],
16517
17284
  colWidths: [40, 9, 24, 70],
16518
17285
  wordWrap: true,
@@ -16540,7 +17307,7 @@ function renderEntry(entry) {
16540
17307
  const blocks = [`${entry.type} (${entry.kind}) \u2014 ${entry.label}`, entry.summary, `Docs: ${entry.docs}`];
16541
17308
  const inputs = Object.entries(entry.inputs);
16542
17309
  if (inputs.length) {
16543
- const table = new import_cli_table35.default({
17310
+ const table = new import_cli_table37.default({
16544
17311
  head: ["Input", "Type", "Required", "Description"],
16545
17312
  colWidths: [20, 9, 10, 90],
16546
17313
  wordWrap: true,
@@ -16790,21 +17557,27 @@ ${item.lines.map((line) => ` ${line}`).join("\n")}`
16790
17557
  }
16791
17558
 
16792
17559
  // src/commands/workspaces.ts
16793
- var import_cli_table36 = __toESM(require_cli_table3(), 1);
17560
+ var import_cli_table38 = __toESM(require_cli_table3(), 1);
16794
17561
  function registerWorkspaces(program3) {
16795
- program3.command("workspaces").description("The workspaces the stored token reaches, with your role in each").action(async (_opts, command) => {
17562
+ program3.command("workspaces").description("The workspaces the stored token reaches, with your role and the scopes the token grants in each").action(async (_opts, command) => {
16796
17563
  const options = globalOptions(command);
16797
17564
  const target = resolveTarget();
16798
17565
  const client = await authenticatedClient(target);
16799
17566
  const workspaces = await listWorkspaces(client);
16800
- const table = new import_cli_table36.default({
16801
- head: ["ID", "Name", "Role", "Default"],
16802
- colWidths: [38, 34, 14, 9],
17567
+ const table = new import_cli_table38.default({
17568
+ head: ["ID", "Name", "Role", "Default", "Scopes"],
17569
+ colWidths: [38, 26, 10, 9, 34],
16803
17570
  wordWrap: true,
16804
17571
  style: { head: [], border: [], "padding-left": 1, "padding-right": 1 }
16805
17572
  });
16806
17573
  for (const workspace of workspaces) {
16807
- table.push([workspace.id, workspace.name, workspace.userRole ?? "", workspace.isDefault ? "yes" : ""]);
17574
+ table.push([
17575
+ workspace.id,
17576
+ workspace.name,
17577
+ workspace.userRole ?? "",
17578
+ workspace.isDefault ? "yes" : "",
17579
+ (workspace.scopes ?? []).join(", ")
17580
+ ]);
16808
17581
  }
16809
17582
  emit(
16810
17583
  options,
@@ -16818,11 +17591,12 @@ Pass --workspace <id> to a command, or set SOFTR_WORKSPACE_ID, to work in one of
16818
17591
  // package.json
16819
17592
  var package_default = {
16820
17593
  name: "@softr/cli",
16821
- version: "0.1.3",
16822
- description: "The Softr command line: build, test and publish Softr workflows from local files; more Softr APIs to come",
17594
+ version: "0.1.5",
17595
+ description: "The Softr command line: build, test and publish Softr workflows from local files, manage Softr Databases; more Softr APIs to come",
16823
17596
  keywords: [
16824
17597
  "softr",
16825
17598
  "workflows",
17599
+ "databases",
16826
17600
  "cli"
16827
17601
  ],
16828
17602
  license: "UNLICENSED",
@@ -16845,7 +17619,7 @@ var package_default = {
16845
17619
  format: "prettier --write .",
16846
17620
  "format:check": "prettier --check .",
16847
17621
  check: "npm run typecheck && npm run format:check && npm test && npm run build",
16848
- "check:skill": "npx -y skills-ref@0.1.5 validate skills/softr-cli && npx -y skills-ref@0.1.5 validate skills/softr-workflows"
17622
+ "check:skill": "npx -y skills-ref@0.1.5 validate skills/softr-cli && npx -y skills-ref@0.1.5 validate skills/softr-workflows && npx -y skills-ref@0.1.5 validate skills/softr-db"
16849
17623
  },
16850
17624
  publishConfig: {
16851
17625
  access: "public"
@@ -16868,7 +17642,7 @@ var package_default = {
16868
17642
  };
16869
17643
 
16870
17644
  // src/cli.ts
16871
- 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");
17645
+ var program2 = new Command().name("softr").description("The Softr command line: workflows and databases today, more of Softr to come").version(package_default.version).option("--json", "print results as JSON");
16872
17646
  registerLogin(program2);
16873
17647
  registerWorkspaces(program2);
16874
17648
  registerIntegrations(program2);
@@ -16888,4 +17662,9 @@ registerPublish(workflows);
16888
17662
  registerPin(workflows);
16889
17663
  registerSpec(workflows);
16890
17664
  registerSchema(workflows);
17665
+ var db = program2.command("db").description("Manage Softr Databases: databases, their tables, fields and records");
17666
+ registerDatabases(db);
17667
+ registerTables(db);
17668
+ registerFields(db);
17669
+ registerViews(db);
16891
17670
  program2.parseAsync(process.argv).catch(reportError);