@softr/cli 0.1.3 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +79 -36
  2. package/dist/cli.mjs +902 -526
  3. package/package.json +3 -2
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 Table8 = 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
+ Table8.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 = Table8;
1833
1833
  }
1834
1834
  });
1835
1835
 
@@ -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
  /**
@@ -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
 
@@ -6963,12 +6989,12 @@ function parseTree(text, errors = [], options = ParseOptions.DEFAULT) {
6963
6989
  }
6964
6990
  return result;
6965
6991
  }
6966
- function findNodeAtLocation(root, path13) {
6992
+ function findNodeAtLocation(root, path15) {
6967
6993
  if (!root) {
6968
6994
  return void 0;
6969
6995
  }
6970
6996
  let node = root;
6971
- for (let segment of path13) {
6997
+ for (let segment of path15) {
6972
6998
  if (typeof segment === "string") {
6973
6999
  if (node.type !== "object" || !Array.isArray(node.children)) {
6974
7000
  return void 0;
@@ -7322,14 +7348,14 @@ function getNodeType(value) {
7322
7348
 
7323
7349
  // node_modules/jsonc-parser/lib/esm/impl/edit.js
7324
7350
  function setProperty(text, originalPath, value, options) {
7325
- const path13 = originalPath.slice();
7351
+ const path15 = originalPath.slice();
7326
7352
  const errors = [];
7327
7353
  const root = parseTree(text, errors);
7328
7354
  let parent = void 0;
7329
7355
  let lastSegment = void 0;
7330
- while (path13.length > 0) {
7331
- lastSegment = path13.pop();
7332
- parent = findNodeAtLocation(root, path13);
7356
+ while (path15.length > 0) {
7357
+ lastSegment = path15.pop();
7358
+ parent = findNodeAtLocation(root, path15);
7333
7359
  if (parent === void 0 && value !== void 0) {
7334
7360
  if (typeof lastSegment === "string") {
7335
7361
  value = { [lastSegment]: value };
@@ -7552,8 +7578,8 @@ function printParseErrorCode(code) {
7552
7578
  }
7553
7579
  return "<unknown ParseErrorCode>";
7554
7580
  }
7555
- function modify(text, path13, value, options) {
7556
- return setProperty(text, path13, value, options);
7581
+ function modify(text, path15, value, options) {
7582
+ return setProperty(text, path15, value, options);
7557
7583
  }
7558
7584
  function applyEdits(text, edits) {
7559
7585
  let sortedEdits = edits.slice(0).sort((a, b) => {
@@ -7583,14 +7609,36 @@ var CONFIG_FILE_NAME = "softr-workflows.jsonc";
7583
7609
  function configPath(dir) {
7584
7610
  return path2.join(dir, CONFIG_FILE_NAME);
7585
7611
  }
7586
- async function configExists(dir) {
7612
+ async function fileExists(file) {
7587
7613
  try {
7588
- await access(configPath(dir));
7614
+ await access(file);
7589
7615
  return true;
7590
7616
  } catch {
7591
7617
  return false;
7592
7618
  }
7593
7619
  }
7620
+ async function configExists(dir) {
7621
+ return fileExists(configPath(dir));
7622
+ }
7623
+ function parseJsoncFile(file, text) {
7624
+ const errors = [];
7625
+ const value = parse2(text, errors, { allowTrailingComma: true });
7626
+ if (errors.length) {
7627
+ const first = errors[0];
7628
+ const line = text.slice(0, first.offset).split("\n").length;
7629
+ throw new CliError(`${file}:${line}: ${printParseErrorCode(first.error)}`);
7630
+ }
7631
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
7632
+ throw new CliError(`${file}: the top level must be a JSON object.`);
7633
+ }
7634
+ return value;
7635
+ }
7636
+ async function listSubdirsWith(cwd, holds) {
7637
+ const entries = await readdir(cwd, { withFileTypes: true });
7638
+ const dirs = entries.filter((entry) => entry.isDirectory()).map((entry) => path2.join(cwd, entry.name));
7639
+ const flags = await Promise.all(dirs.map((dir) => holds(dir)));
7640
+ return dirs.filter((_, index) => flags[index]);
7641
+ }
7594
7642
  async function readConfig(dir) {
7595
7643
  const file = configPath(dir);
7596
7644
  let text;
@@ -7605,17 +7653,7 @@ async function readConfig(dir) {
7605
7653
  }
7606
7654
  throw error;
7607
7655
  }
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;
7656
+ return parseJsoncFile(file, text);
7619
7657
  }
7620
7658
  async function writeConfig(dir, config) {
7621
7659
  const file = configPath(dir);
@@ -7635,10 +7673,7 @@ async function findWorkflowDir(cwd, explicitDir) {
7635
7673
  throw new CliError(`No ${CONFIG_FILE_NAME} in ${cwd}.`, hint);
7636
7674
  }
7637
7675
  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]);
7676
+ return listSubdirsWith(cwd, configExists);
7642
7677
  }
7643
7678
 
7644
7679
  // src/project/config-edit.ts
@@ -13555,8 +13590,8 @@ function typeOfField(field) {
13555
13590
  return field.allowMultipleEntries ? arrayOf(base) : base;
13556
13591
  }
13557
13592
  var SEGMENT = /^(?:\.?\[\s*'((?:[^'\\]|\\.)*)'\s*\]|\.?\[\s*"((?:[^"\\]|\\.)*)"\s*\]|\.?\[\s*(\d+)\s*\]|\.?\[\s*\*\s*\]|\.([^.[\]]+))/;
13558
- function parsePath(path13) {
13559
- let rest = path13.trim();
13593
+ function parsePath(path15) {
13594
+ let rest = path15.trim();
13560
13595
  if (rest.startsWith("$")) {
13561
13596
  rest = rest.slice(1);
13562
13597
  }
@@ -14056,10 +14091,10 @@ function describePlaceholder(placeholder, raw, config, outputs, depth) {
14056
14091
  function untested(nodeId) {
14057
14092
  return `node "${nodeId}" has no test output yet (run: softr workflows test ${nodeId})`;
14058
14093
  }
14059
- function describeOutputRef(nodeId, path13, source, outputs) {
14060
- const segments = parsePath(path13);
14094
+ function describeOutputRef(nodeId, path15, source, outputs) {
14095
+ const segments = parsePath(path15);
14061
14096
  if (!segments) {
14062
- return { type: UNKNOWN, source, verified: false, note: `unsupported path "${path13}"` };
14097
+ return { type: UNKNOWN, source, verified: false, note: `unsupported path "${path15}"` };
14063
14098
  }
14064
14099
  const row = outputs[nodeId];
14065
14100
  if (!row) {
@@ -14384,65 +14419,167 @@ ${notes.join("\n")}` : ""}`,
14384
14419
  });
14385
14420
  }
14386
14421
 
14387
- // src/commands/pull.ts
14388
- import path8 from "node:path";
14389
-
14390
- // src/api/workflows.ts
14391
- function getWorkflow(client, workflowId) {
14392
- return apiRequest(client, "GET", `/v1/workflows/${workflowId}`);
14393
- }
14394
- function updateWorkflow(client, workflowId, body) {
14395
- return apiRequest(client, "PUT", `/v1/workflows/${workflowId}`, body);
14396
- }
14397
- function getWorkflowOutputs(client, workflowId) {
14398
- return apiRequest(client, "GET", `/v1/workflows/${workflowId}/outputs`);
14399
- }
14400
- function listWorkflows(client, workspaceId, { applicationId } = {}) {
14401
- const query = new URLSearchParams({ workspaceId, ...applicationId ? { applicationId } : {}, limit: "500" });
14402
- return apiRequest(client, "GET", `/v1/workflows?${query}`);
14403
- }
14404
- function pinWorkflow(client, workflowId, applicationId) {
14405
- return apiRequestVoid(client, "POST", `/v1/workflows/${workflowId}/pinned-applications/${applicationId}`);
14422
+ // src/project/slug.ts
14423
+ function slug(title, fallback = "workflow") {
14424
+ return title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || fallback;
14406
14425
  }
14407
- function unpinWorkflow(client, workflowId, applicationId) {
14408
- return apiRequestVoid(client, "DELETE", `/v1/workflows/${workflowId}/pinned-applications/${applicationId}`);
14426
+
14427
+ // src/commands/add.ts
14428
+ function uniqueId(base, taken) {
14429
+ if (!taken.has(base)) {
14430
+ return base;
14431
+ }
14432
+ for (let n = 2; ; n++) {
14433
+ if (!taken.has(`${base}-${n}`)) {
14434
+ return `${base}-${n}`;
14435
+ }
14436
+ }
14409
14437
  }
14410
- function testAction(client, workflowId, hierarchyPath2, mode) {
14411
- return apiRequest(client, "POST", `/v1/workflows/${workflowId}/outputs/${mode}/actions/${hierarchyPath2}`, {});
14438
+ function defaultAfter(config, loopId) {
14439
+ const order = executionOrder(config);
14440
+ const container = loopId ? order.filter((node) => node.loopId === loopId) : order.filter((node) => node.loopId === void 0 && node.reachable);
14441
+ if (!container.length) {
14442
+ if (loopId) {
14443
+ return loopId;
14444
+ }
14445
+ const trigger = config.triggers?.[0];
14446
+ if (!trigger) {
14447
+ throw new CliError("The workflow has no trigger to attach the first action to.");
14448
+ }
14449
+ return trigger.id;
14450
+ }
14451
+ const paths = loopId ? [...allActions(config.actions)].find((node) => node.id === loopId)?.inputs?.paths ?? [] : config.paths ?? [];
14452
+ const withOutgoing = new Set(paths.map((p) => p.fromActionId));
14453
+ const leaves = container.filter((node) => !withOutgoing.has(node.id));
14454
+ if (leaves.length === 1 && leaves[0]) {
14455
+ return leaves[0].id;
14456
+ }
14457
+ throw new CliError(
14458
+ leaves.length ? `Several nodes have no successor: ${leaves.map((node) => node.id).join(", ")}.` : "Every node already has a successor.",
14459
+ "Pass --after <nodeId> to say where the new node goes."
14460
+ );
14412
14461
  }
14413
- function testTrigger(client, workflowId, triggerId) {
14414
- return apiRequest(client, "POST", `/v1/workflows/${workflowId}/outputs/triggers/${triggerId}/test`, {});
14462
+ function registerAdd(program3) {
14463
+ 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) => {
14464
+ const options = globalOptions(command);
14465
+ const dir = await findWorkflowDir(process.cwd(), opts.dir);
14466
+ const config = await readConfig(dir);
14467
+ const entry = findCatalogEntry(type);
14468
+ const nodes = [...config.triggers ?? [], ...allActions(config.actions)];
14469
+ const taken = new Set(nodes.map((node2) => node2.id));
14470
+ const id = opts.id ?? uniqueId(entry.kind === "trigger" ? "trigger" : slug(entry.label), taken);
14471
+ if (taken.has(id)) {
14472
+ throw new CliError(`Node id "${id}" is already used.`);
14473
+ }
14474
+ if (id.includes(":::") || /[\s/?#]/.test(id)) {
14475
+ throw new CliError(`Node id "${id}" must not contain ":::", spaces, "/", "?" or "#".`);
14476
+ }
14477
+ const title = opts.title ?? entry.label;
14478
+ const { inputs, codeFile } = await newNodeInputs(entry, id, title, opts.lang, dir);
14479
+ const node = { id, title, type: entry.type, version: entry.version, inputs };
14480
+ if (entry.kind === "trigger") {
14481
+ if (opts.after || opts.inLoop) {
14482
+ throw new CliError(
14483
+ "Triggers start the workflow: use --to <actionId> to connect one, not --after or --in-loop."
14484
+ );
14485
+ }
14486
+ if (opts.to && !taken.has(opts.to)) {
14487
+ throw new CliError(`No node "${opts.to}" to connect to.`, `Known nodes: ${[...taken].join(", ")}`);
14488
+ }
14489
+ await editConfig(dir, [
14490
+ { path: ["triggers", -1], value: node },
14491
+ ...opts.to ? [{ path: ["paths", -1], value: { fromActionId: id, toActionId: opts.to } }] : []
14492
+ ]);
14493
+ const notes = [
14494
+ (config.triggers?.length ?? 0) >= 1 ? "The service accepts one trigger per published workflow for now; multi-trigger support is coming." : "",
14495
+ opts.to ? "" : `Connect it with a path to the first action (or pass --to next time).`,
14496
+ `Then: softr workflows push, softr workflows test ${id}.`
14497
+ ].filter(Boolean);
14498
+ emitWithVersionNote(
14499
+ options,
14500
+ config,
14501
+ `Added ${entry.type} trigger "${id}"${opts.to ? ` leading to "${opts.to}"` : ""}.
14502
+ ${notes.join("\n")}`,
14503
+ {
14504
+ id,
14505
+ kind: "trigger",
14506
+ type: entry.type,
14507
+ to: opts.to ?? null
14508
+ }
14509
+ );
14510
+ return;
14511
+ }
14512
+ if (opts.inLoop) {
14513
+ const loop = [...allActions(config.actions)].find((candidate) => candidate.id === opts.inLoop);
14514
+ if (!loop || loop.type !== LOOP_TYPE) {
14515
+ throw new CliError(`"${opts.inLoop}" is not a loop in this workflow.`);
14516
+ }
14517
+ }
14518
+ const after = opts.after ?? defaultAfter(config, opts.inLoop);
14519
+ if (!taken.has(after)) {
14520
+ throw new CliError(`No node "${after}" to attach to.`, `Known nodes: ${[...taken].join(", ")}`);
14521
+ }
14522
+ const loopIndex = opts.inLoop ? (config.actions ?? []).findIndex((action) => action.id === opts.inLoop) : -1;
14523
+ const base = opts.inLoop && loopIndex >= 0 ? ["actions", loopIndex, "inputs"] : [];
14524
+ await editConfig(dir, [
14525
+ { path: [...base, "actions", -1], value: node },
14526
+ { path: [...base, "paths", -1], value: { fromActionId: after, toActionId: id } }
14527
+ ]);
14528
+ const next = [
14529
+ codeFile ? `Write the function in ${path6.relative(process.cwd(), codeFile)} and map its inputs under inputs.inputData in softr-workflows.jsonc.` : "",
14530
+ entry.type === "BRANCH" || entry.type === "FILTER" ? `Add the condition(s) on the path(s) leaving it in softr-workflows.jsonc (see "spec ${entry.type}").` : "",
14531
+ `Then: softr workflows push, softr workflows test ${id}.`
14532
+ ].filter(Boolean);
14533
+ emitWithVersionNote(
14534
+ options,
14535
+ config,
14536
+ `Added ${entry.type} "${id}" after "${after}"${opts.inLoop ? ` inside loop "${opts.inLoop}"` : ""}.
14537
+ ${next.join("\n")}`,
14538
+ {
14539
+ id,
14540
+ kind: "action",
14541
+ type: entry.type,
14542
+ after,
14543
+ loopId: opts.inLoop ?? null,
14544
+ codeFile: codeFile ? path6.relative(process.cwd(), codeFile) : null
14545
+ }
14546
+ );
14547
+ });
14415
14548
  }
14416
- function saveTriggerSample(client, workflowId, triggerId, sample) {
14417
- return apiRequestVoid(client, "POST", `/v1/workflows/${workflowId}/sample-output/triggers/${triggerId}`, sample);
14549
+
14550
+ // src/commands/db/databases.ts
14551
+ var import_cli_table3 = __toESM(require_cli_table3(), 1);
14552
+ import path11 from "node:path";
14553
+
14554
+ // src/api/tables.ts
14555
+ var DATABASES = "/api/v1/databases";
14556
+ async function listDatabases(client) {
14557
+ return (await tablesRequest(client, "GET", DATABASES)).data;
14418
14558
  }
14419
- function createWorkflow(client, body) {
14420
- return apiRequest(client, "POST", "/v1/workflows", body);
14559
+ async function getDatabase(client, databaseId) {
14560
+ return (await tablesRequest(client, "GET", `${DATABASES}/${encodeURIComponent(databaseId)}`)).data;
14421
14561
  }
14422
- function enableWorkflow(client, workflowId) {
14423
- return apiRequestVoid(client, "PUT", `/v1/workflows/${workflowId}/enabled`);
14562
+ async function createDatabase(client, database) {
14563
+ return (await tablesRequest(client, "POST", DATABASES, database)).data;
14424
14564
  }
14425
- function disableWorkflow(client, workflowId) {
14426
- return apiRequestVoid(client, "DELETE", `/v1/workflows/${workflowId}/enabled`);
14565
+ async function updateDatabase(client, databaseId, changes) {
14566
+ return (await tablesRequest(client, "PUT", `${DATABASES}/${encodeURIComponent(databaseId)}`, changes)).data;
14427
14567
  }
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
- }
14568
+ async function deleteDatabase(client, databaseId, force) {
14569
+ await tablesRequestVoid(
14570
+ client,
14571
+ "DELETE",
14572
+ `${DATABASES}/${encodeURIComponent(databaseId)}${force ? "?force=true" : ""}`
14573
+ );
14437
14574
  }
14438
- function getWorkflowReadiness(client, workflowId) {
14439
- return apiRequest(client, "GET", `/v1/workflows/${workflowId}/readiness`);
14575
+ async function listTables(client, databaseId) {
14576
+ return (await tablesRequest(client, "GET", `${DATABASES}/${encodeURIComponent(databaseId)}/tables`)).data;
14440
14577
  }
14441
14578
 
14442
14579
  // src/config/credentials.ts
14443
14580
  import { chmod, mkdir as mkdir4, readFile as readFile5, writeFile as writeFile5 } from "node:fs/promises";
14444
14581
  import { homedir } from "node:os";
14445
- import path6 from "node:path";
14582
+ import path7 from "node:path";
14446
14583
 
14447
14584
  // src/api/oauth.ts
14448
14585
  import { createHash as createHash2, randomBytes } from "node:crypto";
@@ -14475,8 +14612,8 @@ function refreshFailureHint(status) {
14475
14612
  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
14613
  }
14477
14614
  async function tokenRequest(target, form) {
14478
- const path13 = "/v1/oauth/token";
14479
- const response = await fetch(`${target.studioApiUrl}${path13}`, {
14615
+ const path15 = "/v1/oauth/token";
14616
+ const response = await fetch(`${target.studioApiUrl}${path15}`, {
14480
14617
  method: "POST",
14481
14618
  headers: { "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json" },
14482
14619
  body: new URLSearchParams({ ...form, client_id: oauthClientId() }).toString()
@@ -14484,7 +14621,7 @@ async function tokenRequest(target, form) {
14484
14621
  if (!response.ok) {
14485
14622
  const text = await response.text();
14486
14623
  throw new SoftrApiError(
14487
- `POST ${path13} failed with status ${response.status}${text ? `: ${text}` : ""}`,
14624
+ `POST ${path15} failed with status ${response.status}${text ? `: ${text}` : ""}`,
14488
14625
  response.status,
14489
14626
  text,
14490
14627
  form["grant_type"] === "refresh_token" ? refreshFailureHint(response.status) : void 0
@@ -14513,9 +14650,11 @@ function refreshTokens(target, refreshToken) {
14513
14650
  var API_URL_ENV_VAR = "SOFTR_WORKFLOWS_API_URL";
14514
14651
  var STUDIO_API_URL_ENV_VAR = "SOFTR_STUDIO_API_URL";
14515
14652
  var STUDIO_URL_ENV_VAR = "SOFTR_STUDIO_URL";
14653
+ var TABLES_API_URL_ENV_VAR = "SOFTR_TABLES_API_URL";
14516
14654
  var DEFAULT_API_URL = "https://workflows-api.softr.io";
14517
14655
  var DEFAULT_STUDIO_API_URL = "https://studio-api.softr.io";
14518
14656
  var DEFAULT_STUDIO_URL = "https://studio.softr.io";
14657
+ var DEFAULT_TABLES_API_URL = "https://tables-api.softr.io";
14519
14658
  function normalizeUrl(value, source) {
14520
14659
  let url;
14521
14660
  try {
@@ -14533,7 +14672,8 @@ function resolveTarget(remembered) {
14533
14672
  return {
14534
14673
  apiUrl: pick(API_URL_ENV_VAR, remembered?.apiUrl, DEFAULT_API_URL),
14535
14674
  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)
14675
+ studioUrl: pick(STUDIO_URL_ENV_VAR, remembered?.studioUrl, DEFAULT_STUDIO_URL),
14676
+ tablesApiUrl: pick(TABLES_API_URL_ENV_VAR, remembered?.tablesApiUrl, DEFAULT_TABLES_API_URL)
14537
14677
  };
14538
14678
  }
14539
14679
  function targetHost(target) {
@@ -14563,7 +14703,7 @@ function isProxyAuth() {
14563
14703
  }
14564
14704
  var REFRESH_MARGIN_MS = 5 * 60 * 1e3;
14565
14705
  function credentialsPath() {
14566
- return path6.join(homedir(), ".softr", "credentials.json");
14706
+ return path7.join(homedir(), ".softr", "credentials.json");
14567
14707
  }
14568
14708
  async function readCredentials() {
14569
14709
  try {
@@ -14577,7 +14717,7 @@ async function readCredentials() {
14577
14717
  }
14578
14718
  async function writeCredentials(credentials) {
14579
14719
  const file = credentialsPath();
14580
- await mkdir4(path6.dirname(file), { recursive: true, mode: 448 });
14720
+ await mkdir4(path7.dirname(file), { recursive: true, mode: 448 });
14581
14721
  await writeFile5(file, JSON.stringify(credentials, null, 2) + "\n", { mode: 384 });
14582
14722
  await chmod(file, 384);
14583
14723
  return file;
@@ -14650,51 +14790,46 @@ async function authenticatedClient(target) {
14650
14790
  return { target, token: stored.token, kind };
14651
14791
  }
14652
14792
 
14653
- // src/commands/list.ts
14654
- var import_cli_table3 = __toESM(require_cli_table3(), 1);
14655
-
14656
- // src/api/studio.ts
14657
- async function resolveTokenOwner(client) {
14793
+ // src/project/db-dir.ts
14794
+ import { mkdir as mkdir5, readFile as readFile6, rm, writeFile as writeFile6 } from "node:fs/promises";
14795
+ import path8 from "node:path";
14796
+ var DB_CONFIG_FILE_NAME = "softr-db.jsonc";
14797
+ var DB_CACHE_DIR_NAME = ".softr-db";
14798
+ var DATABASE_ID_ENV_VAR = "SOFTR_DATABASE_ID";
14799
+ function configFile(dir) {
14800
+ return path8.join(dir, DB_CONFIG_FILE_NAME);
14801
+ }
14802
+ function cacheFile(dir) {
14803
+ return path8.join(dir, DB_CACHE_DIR_NAME, "schema.json");
14804
+ }
14805
+ async function dbConfigExists(dir) {
14806
+ return fileExists(configFile(dir));
14807
+ }
14808
+ async function readDbConfig(dir) {
14809
+ const file = configFile(dir);
14810
+ let text;
14658
14811
  try {
14659
- return await studioRequest(client, "POST", "/v1/users/me/token", { token: client.token });
14812
+ text = await readFile6(file, "utf8");
14660
14813
  } 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
- );
14814
+ if (error.code === "ENOENT") {
14815
+ return void 0;
14666
14816
  }
14667
14817
  throw error;
14668
14818
  }
14819
+ const value = parseJsoncFile(file, text);
14820
+ if (typeof value["databaseId"] !== "string" || !value["databaseId"]) {
14821
+ throw new CliError(`${file}: "databaseId" must be a non-empty string.`);
14822
+ }
14823
+ return { databaseId: value["databaseId"] };
14669
14824
  }
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
- }));
14683
- }
14684
- function listWorkspaceIntegrations(client, workspaceId) {
14685
- return studioRequest(client, "GET", `/v1/api/me/workspaces/${workspaceId}/integrations`);
14686
- }
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);
14825
+ async function writeDbConfig(dir, config) {
14826
+ await mkdir5(dir, { recursive: true });
14827
+ await writeFile6(configFile(dir), JSON.stringify(config, null, 2) + "\n");
14828
+ return configFile(dir);
14694
14829
  }
14695
- async function readState(dir) {
14830
+ async function readSchemaCache(dir) {
14696
14831
  try {
14697
- return JSON.parse(await readFile6(path7.join(stateDir(dir), "state.json"), "utf8"));
14832
+ return JSON.parse(await readFile6(cacheFile(dir), "utf8"));
14698
14833
  } catch (error) {
14699
14834
  if (error.code === "ENOENT") {
14700
14835
  return void 0;
@@ -14702,127 +14837,250 @@ async function readState(dir) {
14702
14837
  throw error;
14703
14838
  }
14704
14839
  }
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");
14840
+ async function writeSchemaCache(dir, database, tables) {
14841
+ const cache = { database, tables, refreshedAt: (/* @__PURE__ */ new Date()).toISOString() };
14842
+ await mkdir5(path8.dirname(cacheFile(dir)), { recursive: true });
14843
+ await writeFile6(cacheFile(dir), JSON.stringify(cache, null, 2) + "\n");
14844
+ return cache;
14709
14845
  }
14710
- async function readStateFile(dir, name) {
14711
- try {
14712
- return JSON.parse(await readFile6(path7.join(stateDir(dir), name), "utf8"));
14713
- } catch (error) {
14714
- if (error.code === "ENOENT") {
14715
- return void 0;
14716
- }
14717
- throw error;
14846
+ async function refreshSchemaCache(client, dir, databaseId) {
14847
+ const [database, tables] = await Promise.all([getDatabase(client, databaseId), listTables(client, databaseId)]);
14848
+ return writeSchemaCache(dir, database, tables);
14849
+ }
14850
+ async function removeSchemaCache(dir) {
14851
+ await rm(path8.join(dir, DB_CACHE_DIR_NAME), { recursive: true, force: true });
14852
+ }
14853
+ async function resolveDatabase(cwd, databaseId, explicitDir) {
14854
+ const dir = path8.resolve(cwd, explicitDir ?? ".");
14855
+ const config = await readDbConfig(dir);
14856
+ if (explicitDir && !config) {
14857
+ throw new CliError(`No ${DB_CONFIG_FILE_NAME} in ${dir}.`, 'Run "softr db get <database>" to create one.');
14858
+ }
14859
+ const given = databaseId ?? (process.env[DATABASE_ID_ENV_VAR] || void 0);
14860
+ if (given) {
14861
+ return config?.databaseId === given ? { databaseId: given, dir } : { databaseId: given };
14718
14862
  }
14863
+ if (config) {
14864
+ return { databaseId: config.databaseId, dir };
14865
+ }
14866
+ const candidates = await listSubdirsWith(cwd, dbConfigExists);
14867
+ throw new CliError(
14868
+ `Which database? There is no ${DB_CONFIG_FILE_NAME} in ${cwd}.`,
14869
+ 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>".`
14870
+ );
14719
14871
  }
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;
14872
+ function belongsToAnother(dir, existingId, wantedId, hint) {
14873
+ return new CliError(
14874
+ `${path8.join(dir, DB_CONFIG_FILE_NAME)} belongs to database ${existingId}${wantedId ? `, not ${wantedId}` : ""}.`,
14875
+ hint
14876
+ );
14725
14877
  }
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
- };
14878
+ async function databaseDirFor(cwd, explicitDir, databaseId, dirName) {
14879
+ const here = path8.resolve(cwd, explicitDir ?? ".");
14880
+ const inHere = await readDbConfig(here);
14881
+ if (inHere) {
14882
+ if (databaseId && inHere.databaseId === databaseId) {
14883
+ return { dir: here, existing: true };
14884
+ }
14885
+ throw belongsToAnother(
14886
+ here,
14887
+ inHere.databaseId,
14888
+ databaseId,
14889
+ explicitDir ? "Pick another --dir." : "Run it from the parent directory (a new subdirectory is created) or pass --dir."
14890
+ );
14891
+ }
14892
+ if (explicitDir) {
14893
+ return { dir: here, existing: false };
14894
+ }
14895
+ const sibling2 = path8.join(cwd, dirName);
14896
+ const inSibling = await readDbConfig(sibling2);
14897
+ if (inSibling) {
14898
+ if (inSibling.databaseId === databaseId) {
14899
+ return { dir: sibling2, existing: true };
14900
+ }
14901
+ throw belongsToAnother(sibling2, inSibling.databaseId, databaseId, "Pass --dir <path>.");
14902
+ }
14903
+ return { dir: sibling2, existing: false };
14740
14904
  }
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) {
14905
+ async function findReachableDatabase(client, reference) {
14906
+ const reachable = await listDatabases(client);
14907
+ const byId = reachable.find((database) => database.id === reference);
14908
+ if (byId) {
14909
+ return byId;
14910
+ }
14911
+ const byName = reachable.filter((database) => database.name === reference);
14912
+ if (byName.length > 1) {
14745
14913
  throw new CliError(
14746
- `You have no access to workspace ${workspaceId}.`,
14747
- `Known workspaces: ${workspaces.map((w) => `${w.name} (${w.id})`).join(", ")}`
14914
+ `Several databases are named "${reference}".`,
14915
+ `Pass the id of one: ${byName.map((database) => `${database.id} (workspace ${database.workspaceId})`).join(", ")}`
14748
14916
  );
14749
14917
  }
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
- });
14918
+ if (!byName[0]) {
14919
+ throw new CliError(
14920
+ `The token reaches no database with the id or name "${reference}".`,
14921
+ '"softr db list" shows the ones it reaches.'
14922
+ );
14923
+ }
14924
+ return byName[0];
14925
+ }
14926
+ async function bindDatabaseDir(client, cwd, reference, explicitDir) {
14927
+ const here = path8.resolve(cwd, explicitDir ?? ".");
14928
+ const config = await readDbConfig(here);
14929
+ if (!reference || reference === config?.databaseId) {
14930
+ if (!config) {
14931
+ const candidates = await listSubdirsWith(cwd, dbConfigExists);
14932
+ throw new CliError(
14933
+ `Which database? There is no ${DB_CONFIG_FILE_NAME} in ${here}.`,
14934
+ 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.'
14935
+ );
14766
14936
  }
14937
+ return { databaseId: config.databaseId, dir: here, wrote: false };
14767
14938
  }
14768
- return listing;
14939
+ const database = await findReachableDatabase(client, reference);
14940
+ const { dir, existing } = await databaseDirFor(cwd, explicitDir, database.id, slug(database.name, "database"));
14941
+ if (!existing) {
14942
+ await writeDbConfig(dir, { databaseId: database.id });
14943
+ }
14944
+ return { databaseId: database.id, dir, wrote: !existing };
14769
14945
  }
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
- ]);
14946
+
14947
+ // src/api/studio.ts
14948
+ async function resolveTokenOwner(client) {
14949
+ try {
14950
+ return await studioRequest(client, "POST", "/v1/users/me/token", { token: client.token });
14951
+ } catch (error) {
14952
+ if (error instanceof SoftrApiError && error.status === 404) {
14953
+ throw new CliError(
14954
+ "The studio does not know this token: revoked, expired, or from another environment.",
14955
+ 'Create a new personal API token in the studio, or run "softr login" for the browser login.'
14956
+ );
14957
+ }
14958
+ throw error;
14788
14959
  }
14789
- return table.toString();
14790
14960
  }
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)`);
14961
+ var WORKFLOWS_READ_SCOPE = "workflows:read";
14962
+ var DATABASES_SCHEMA_WRITE_SCOPE = "databases.schema:write";
14963
+ function grantsActionOf(statements, prefix) {
14964
+ return (statements ?? []).some(
14965
+ (statement) => statement.actions.some((action) => action === "*" || action.startsWith(prefix))
14966
+ );
14967
+ }
14968
+ function grantsWorkflows(statements) {
14969
+ return grantsActionOf(statements, "workflows:");
14970
+ }
14971
+ function grantsDatabases(statements) {
14972
+ return grantsActionOf(statements, "databases.");
14973
+ }
14974
+ async function listWorkspacesWith(client, scope) {
14975
+ const reachable = await listWorkspaces(client);
14976
+ return { reachable, granted: reachable.filter((workspace) => !workspace.scopes || workspace.scopes.includes(scope)) };
14977
+ }
14978
+ async function listWorkspaces(client) {
14979
+ const workspaces = await studioRequest(client, "GET", "/v1/api/me/workspaces");
14980
+ return workspaces.map((workspace) => ({
14981
+ id: workspace.id,
14982
+ name: workspace.name,
14983
+ ...workspace.role ? { userRole: workspace.role } : {},
14984
+ isDefault: workspace.isDefault,
14985
+ ...workspace.scopes ? { scopes: workspace.scopes } : {}
14986
+ }));
14987
+ }
14988
+ function listWorkspaceIntegrations(client, workspaceId) {
14989
+ return studioRequest(client, "GET", `/v1/api/me/workspaces/${workspaceId}/integrations`);
14990
+ }
14991
+
14992
+ // src/commands/init.ts
14993
+ import path10 from "node:path";
14994
+
14995
+ // src/api/workflows.ts
14996
+ function getWorkflow(client, workflowId) {
14997
+ return apiRequest(client, "GET", `/v1/workflows/${workflowId}`);
14998
+ }
14999
+ function updateWorkflow(client, workflowId, body) {
15000
+ return apiRequest(client, "PUT", `/v1/workflows/${workflowId}`, body);
15001
+ }
15002
+ function getWorkflowOutputs(client, workflowId) {
15003
+ return apiRequest(client, "GET", `/v1/workflows/${workflowId}/outputs`);
15004
+ }
15005
+ function listWorkflows(client, workspaceId, { applicationId } = {}) {
15006
+ const query = new URLSearchParams({ workspaceId, ...applicationId ? { applicationId } : {}, limit: "500" });
15007
+ return apiRequest(client, "GET", `/v1/workflows?${query}`);
15008
+ }
15009
+ function pinWorkflow(client, workflowId, applicationId) {
15010
+ return apiRequestVoid(client, "POST", `/v1/workflows/${workflowId}/pinned-applications/${applicationId}`);
15011
+ }
15012
+ function unpinWorkflow(client, workflowId, applicationId) {
15013
+ return apiRequestVoid(client, "DELETE", `/v1/workflows/${workflowId}/pinned-applications/${applicationId}`);
15014
+ }
15015
+ function testAction(client, workflowId, hierarchyPath2, mode) {
15016
+ return apiRequest(client, "POST", `/v1/workflows/${workflowId}/outputs/${mode}/actions/${hierarchyPath2}`, {});
15017
+ }
15018
+ function testTrigger(client, workflowId, triggerId) {
15019
+ return apiRequest(client, "POST", `/v1/workflows/${workflowId}/outputs/triggers/${triggerId}/test`, {});
15020
+ }
15021
+ function saveTriggerSample(client, workflowId, triggerId, sample) {
15022
+ return apiRequestVoid(client, "POST", `/v1/workflows/${workflowId}/sample-output/triggers/${triggerId}`, sample);
15023
+ }
15024
+ function createWorkflow(client, body) {
15025
+ return apiRequest(client, "POST", "/v1/workflows", body);
15026
+ }
15027
+ function enableWorkflow(client, workflowId) {
15028
+ return apiRequestVoid(client, "PUT", `/v1/workflows/${workflowId}/enabled`);
15029
+ }
15030
+ function disableWorkflow(client, workflowId) {
15031
+ return apiRequestVoid(client, "DELETE", `/v1/workflows/${workflowId}/enabled`);
15032
+ }
15033
+ async function getEnabledVersion(client, workflowId) {
15034
+ try {
15035
+ return await apiRequest(client, "GET", `/v1/workflows/${workflowId}/enabled`);
15036
+ } catch (error) {
15037
+ if (error instanceof SoftrApiError && error.status === 404) {
15038
+ return null;
15039
+ }
15040
+ throw error;
14799
15041
  }
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
- );
15042
+ }
15043
+ function getWorkflowReadiness(client, workflowId) {
15044
+ return apiRequest(client, "GET", `/v1/workflows/${workflowId}/readiness`);
15045
+ }
15046
+
15047
+ // src/project/state.ts
15048
+ import { mkdir as mkdir6, readFile as readFile7, writeFile as writeFile7 } from "node:fs/promises";
15049
+ import path9 from "node:path";
15050
+ var STATE_DIR_NAME = ".softr-workflows";
15051
+ function stateDir(dir) {
15052
+ return path9.join(dir, STATE_DIR_NAME);
15053
+ }
15054
+ async function readState(dir) {
15055
+ try {
15056
+ return JSON.parse(await readFile7(path9.join(stateDir(dir), "state.json"), "utf8"));
15057
+ } catch (error) {
15058
+ if (error.code === "ENOENT") {
15059
+ return void 0;
15060
+ }
15061
+ throw error;
14809
15062
  }
14810
- return blocks.join("\n\n");
14811
15063
  }
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 });
15064
+ async function writeState(dir, state) {
15065
+ await mkdir6(stateDir(dir), { recursive: true });
15066
+ const value = { ...state, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
15067
+ await writeFile7(path9.join(stateDir(dir), "state.json"), JSON.stringify(value, null, 2) + "\n");
14815
15068
  }
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
- });
15069
+ async function readStateFile(dir, name) {
15070
+ try {
15071
+ return JSON.parse(await readFile7(path9.join(stateDir(dir), name), "utf8"));
15072
+ } catch (error) {
15073
+ if (error.code === "ENOENT") {
15074
+ return void 0;
15075
+ }
15076
+ throw error;
15077
+ }
15078
+ }
15079
+ async function writeStateFile(dir, name, value) {
15080
+ await mkdir6(stateDir(dir), { recursive: true });
15081
+ const file = path9.join(stateDir(dir), name);
15082
+ await writeFile7(file, JSON.stringify(value, null, 2) + "\n");
15083
+ return file;
14826
15084
  }
14827
15085
 
14828
15086
  // src/catalog/schema.ts
@@ -15038,244 +15296,57 @@ function buildSchema() {
15038
15296
 
15039
15297
  // src/commands/schema.ts
15040
15298
  async function writeSchemaFile(dir) {
15041
- return writeStateFile(dir, SCHEMA_FILE, buildSchema());
15042
- }
15043
- var SCHEMA_POINTER = "./.softr-workflows/schema.json";
15044
- function registerSchema(program3) {
15045
- program3.command("schema").description(
15046
- "Print the JSON schema of softr-workflows.jsonc (generated from the node catalog), or write it into a workflow directory"
15047
- ).option("--write", "write .softr-workflows/schema.json in the workflow directory instead of printing").option("--dir <path>", "workflow directory for --write (default: the current directory)").action(async (opts, command) => {
15048
- globalOptions(command);
15049
- if (opts.write) {
15050
- const file = await writeSchemaFile(await findWorkflowDir(process.cwd(), opts.dir));
15051
- process.stdout.write(`Wrote ${file}
15052
- `);
15053
- return;
15054
- }
15055
- process.stdout.write(JSON.stringify(buildSchema(), null, 2) + "\n");
15056
- });
15057
- }
15058
-
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);
15066
- }
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;
15076
- }
15077
- return path8.join(cwd, slug(title));
15078
- }
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;
15143
- }
15144
- for (let n = 2; ; n++) {
15145
- if (!taken.has(`${base}-${n}`)) {
15146
- return `${base}-${n}`;
15147
- }
15148
- }
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;
15162
- }
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;
15168
- }
15169
- 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."
15172
- );
15173
- }
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(", ")}`);
15200
- }
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(
15211
- options,
15212
- config,
15213
- `Added ${entry.type} trigger "${id}"${opts.to ? ` leading to "${opts.to}"` : ""}.
15214
- ${notes.join("\n")}`,
15215
- {
15216
- id,
15217
- kind: "trigger",
15218
- type: entry.type,
15219
- to: opts.to ?? null
15220
- }
15221
- );
15222
- return;
15223
- }
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.`);
15228
- }
15229
- }
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(", ")}`);
15233
- }
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(
15246
- 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
- }
15258
- );
15299
+ return writeStateFile(dir, SCHEMA_FILE, buildSchema());
15300
+ }
15301
+ var SCHEMA_POINTER = "./.softr-workflows/schema.json";
15302
+ function registerSchema(program3) {
15303
+ program3.command("schema").description(
15304
+ "Print the JSON schema of softr-workflows.jsonc (generated from the node catalog), or write it into a workflow directory"
15305
+ ).option("--write", "write .softr-workflows/schema.json in the workflow directory instead of printing").option("--dir <path>", "workflow directory for --write (default: the current directory)").action(async (opts, command) => {
15306
+ globalOptions(command);
15307
+ if (opts.write) {
15308
+ const file = await writeSchemaFile(await findWorkflowDir(process.cwd(), opts.dir));
15309
+ process.stdout.write(`Wrote ${file}
15310
+ `);
15311
+ return;
15312
+ }
15313
+ process.stdout.write(JSON.stringify(buildSchema(), null, 2) + "\n");
15259
15314
  });
15260
15315
  }
15261
15316
 
15262
15317
  // src/commands/init.ts
15263
- import path10 from "node:path";
15264
15318
  var TRIGGER_ID = "trigger";
15265
- async function resolveWorkspace(client, requested) {
15266
- const workspaces = await listWorkspaces(client);
15319
+ function requireGrantedWorkspace({ reachable, granted }, requested, scope) {
15320
+ if (granted.some((workspace) => workspace.id === requested)) {
15321
+ return;
15322
+ }
15323
+ if (reachable.some((workspace) => workspace.id === requested)) {
15324
+ throw new CliError(
15325
+ `The token does not grant ${scope} on workspace ${requested}.`,
15326
+ `Check the token's grants with "softr whoami", or run "softr login" again.`
15327
+ );
15328
+ }
15329
+ throw new CliError(
15330
+ `You have no access to workspace ${requested}.`,
15331
+ `Known workspaces: ${granted.map((w) => `${w.name} (${w.id})`).join(", ")}`
15332
+ );
15333
+ }
15334
+ async function resolveWorkspace(client, requested, scope = WORKFLOWS_READ_SCOPE) {
15335
+ const listing = await listWorkspacesWith(client, scope);
15336
+ const workspaces = listing.granted;
15267
15337
  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
- );
15273
- }
15338
+ requireGrantedWorkspace(listing, requested, scope);
15274
15339
  return requested;
15275
15340
  }
15276
15341
  if (workspaces.length === 1 && workspaces[0]) {
15277
15342
  return workspaces[0].id;
15278
15343
  }
15344
+ if (!workspaces.length) {
15345
+ throw new CliError(
15346
+ `The token grants ${scope} on no workspace.`,
15347
+ `Check the token's grants with "softr whoami", or run "softr login" again.`
15348
+ );
15349
+ }
15279
15350
  const preferred = workspaces.find((workspace) => workspace.isDefault);
15280
15351
  if (preferred) {
15281
15352
  return preferred.id;
@@ -15316,7 +15387,7 @@ function registerInit(program3) {
15316
15387
  });
15317
15388
  const workflow = await getWorkflow(client, created.id);
15318
15389
  const config = { $schema: SCHEMA_POINTER, ...toLocalConfig(workflow) };
15319
- const configFile = await writeConfig(dir, config);
15390
+ const configFile2 = await writeConfig(dir, config);
15320
15391
  await writeSchemaFile(dir);
15321
15392
  const syncedHash = definitionHash((await packWorkflow(config, dir)).packed);
15322
15393
  await writeState(dir, {
@@ -15330,7 +15401,7 @@ function registerInit(program3) {
15330
15401
  emit(
15331
15402
  options,
15332
15403
  `Created "${title}" (${created.id}) with a ${trigger.type} trigger "${TRIGGER_ID}" in workspace ${workspaceId}${pinToApplicationId ? `, linked to application ${pinToApplicationId}` : ""}.
15333
- ${path10.relative(cwd, configFile)}
15404
+ ${path10.relative(cwd, configFile2)}
15334
15405
  ${studioUrl}
15335
15406
  Next: softr workflows add <TYPE> --dir ${path10.relative(cwd, dir) || "."} (see "spec" for the types), then push and test.`,
15336
15407
  {
@@ -15348,10 +15419,129 @@ Next: softr workflows add <TYPE> --dir ${path10.relative(cwd, dir) || "."} (see
15348
15419
  );
15349
15420
  }
15350
15421
 
15422
+ // src/commands/db/databases.ts
15423
+ var DATABASE_ARGUMENT = `the database id (default: $SOFTR_DATABASE_ID, else the one in ${DB_CONFIG_FILE_NAME})`;
15424
+ var DIR_OPTION = `database directory, the one holding ${DB_CONFIG_FILE_NAME} (default: the current directory)`;
15425
+ function renderDatabases(databases) {
15426
+ if (!databases.length) {
15427
+ return "The token reaches no database. Create one with: softr db create <name>";
15428
+ }
15429
+ const table = new import_cli_table3.default({
15430
+ head: ["ID", "Name", "Workspace", "Tables"],
15431
+ colWidths: [38, 34, 38, 8],
15432
+ wordWrap: true,
15433
+ style: { head: [], border: [], "padding-left": 1, "padding-right": 1 }
15434
+ });
15435
+ for (const database of databases) {
15436
+ table.push([database.id, database.name, database.workspaceId, String(database.tablesCount)]);
15437
+ }
15438
+ return table.toString();
15439
+ }
15440
+ function renderDatabase(database, tables) {
15441
+ return [
15442
+ `${database.name} (${database.id})`,
15443
+ ...database.description ? [` ${database.description}`] : [],
15444
+ ` Workspace: ${database.workspaceId}`,
15445
+ ` Created: ${database.createdAt}`,
15446
+ ` Updated: ${database.updatedAt}`,
15447
+ ` Tables: ${tables.length}`,
15448
+ ...tables.map((table) => ` ${table.name} (${table.id}), ${table.fields.length} fields`)
15449
+ ].join("\n");
15450
+ }
15451
+ function relativeDir(cwd, dir) {
15452
+ return path11.relative(cwd, dir) || ".";
15453
+ }
15454
+ function registerDatabases(db2) {
15455
+ db2.command("list").description("List the databases the token reaches, across all workspaces").action(async (_opts, command) => {
15456
+ const options = globalOptions(command);
15457
+ const client = await authenticatedClient(resolveTarget());
15458
+ const databases = await listDatabases(client);
15459
+ emit(options, renderDatabases(databases), { databases });
15460
+ });
15461
+ db2.command("get").description(
15462
+ `Show a database with its tables and keep its directory: ${DB_CONFIG_FILE_NAME} and the schema cache in ${DB_CACHE_DIR_NAME}/`
15463
+ ).argument(
15464
+ "[database]",
15465
+ `the database id or exact name (default: $SOFTR_DATABASE_ID, else the one in ${DB_CONFIG_FILE_NAME})`
15466
+ ).option(
15467
+ "--dir <path>",
15468
+ "database directory (default: this directory if it holds a database, else a new ./<name-slug> for the one given)"
15469
+ ).action(async (argument, opts, command) => {
15470
+ const options = globalOptions(command);
15471
+ const cwd = process.cwd();
15472
+ const reference = argument ?? (process.env[DATABASE_ID_ENV_VAR] || void 0);
15473
+ const client = await authenticatedClient(resolveTarget());
15474
+ const { databaseId, dir, wrote } = await bindDatabaseDir(client, cwd, reference, opts.dir);
15475
+ const { database, tables } = await refreshSchemaCache(client, dir, databaseId);
15476
+ const note = wrote ? `
15477
+ Wrote ${path11.join(relativeDir(cwd, dir), DB_CONFIG_FILE_NAME)}.` : "";
15478
+ emit(options, renderDatabase(database, tables) + note, { database, tables, dir });
15479
+ });
15480
+ 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(
15481
+ "--workspace <id>",
15482
+ "workspace to create it in (default: $SOFTR_WORKSPACE_ID, else your only or default workspace)"
15483
+ ).option("--dir <path>", "directory to create (default: ./<name-slug>)").action(
15484
+ async (name, opts, command) => {
15485
+ const options = globalOptions(command);
15486
+ const cwd = process.cwd();
15487
+ const { dir } = await databaseDirFor(cwd, opts.dir, void 0, slug(name, "database"));
15488
+ const client = await authenticatedClient(resolveTarget());
15489
+ const workspaceId = await resolveWorkspace(
15490
+ client,
15491
+ opts.workspace ?? process.env["SOFTR_WORKSPACE_ID"],
15492
+ DATABASES_SCHEMA_WRITE_SCOPE
15493
+ );
15494
+ const database = await createDatabase(client, { workspaceId, name, description: opts.description });
15495
+ await writeDbConfig(dir, { databaseId: database.id });
15496
+ await refreshSchemaCache(client, dir, database.id);
15497
+ emit(
15498
+ options,
15499
+ `Created database ${database.name} (${database.id}) in workspace ${workspaceId}.
15500
+ ${path11.join(relativeDir(cwd, dir), DB_CONFIG_FILE_NAME)}`,
15501
+ { database, dir }
15502
+ );
15503
+ }
15504
+ );
15505
+ db2.command("update").description("Change a database's name or description").argument("[databaseId]", DATABASE_ARGUMENT).option("--name <name>", "the new name").option("--description <text>", "the new description").option("--dir <path>", DIR_OPTION).action(
15506
+ async (databaseId, opts, command) => {
15507
+ if (!opts.name && !opts.description) {
15508
+ throw new CliError("Nothing to change: pass --name, --description or both.");
15509
+ }
15510
+ const options = globalOptions(command);
15511
+ const target = await resolveDatabase(process.cwd(), databaseId, opts.dir);
15512
+ const client = await authenticatedClient(resolveTarget());
15513
+ const database = await updateDatabase(client, target.databaseId, {
15514
+ name: opts.name,
15515
+ description: opts.description
15516
+ });
15517
+ if (target.dir) {
15518
+ const cache = await readSchemaCache(target.dir);
15519
+ await (cache ? writeSchemaCache(target.dir, database, cache.tables) : refreshSchemaCache(client, target.dir, database.id));
15520
+ }
15521
+ emit(options, `Updated database ${database.name} (${database.id}).`, { database });
15522
+ }
15523
+ );
15524
+ db2.command("delete").description("Delete a database; one that still has tables needs --force").argument("[databaseId]", DATABASE_ARGUMENT).option("--force", "also delete its tables and all their records").option("--dir <path>", DIR_OPTION).action(async (databaseId, opts, command) => {
15525
+ const options = globalOptions(command);
15526
+ const target = await resolveDatabase(process.cwd(), databaseId, opts.dir);
15527
+ const client = await authenticatedClient(resolveTarget());
15528
+ await deleteDatabase(client, target.databaseId, opts.force === true);
15529
+ if (target.dir) {
15530
+ await removeSchemaCache(target.dir);
15531
+ }
15532
+ emit(
15533
+ options,
15534
+ `Deleted database ${target.databaseId}.` + (target.dir ? `
15535
+ ${path11.join(relativeDir(process.cwd(), target.dir), DB_CONFIG_FILE_NAME)} still names the deleted database; delete the directory.` : ""),
15536
+ { deleted: target.databaseId }
15537
+ );
15538
+ });
15539
+ }
15540
+
15351
15541
  // src/commands/integrations.ts
15352
15542
  var import_cli_table32 = __toESM(require_cli_table3(), 1);
15353
- import path11 from "node:path";
15354
- function toRow2(integration) {
15543
+ import path12 from "node:path";
15544
+ function toRow(integration) {
15355
15545
  return {
15356
15546
  id: integration.id,
15357
15547
  name: integration.name,
@@ -15382,7 +15572,7 @@ Use the ID as the integrationId input of a node (see "spec <TYPE>").`;
15382
15572
  }
15383
15573
  async function workflowDirOf(cwd, dir) {
15384
15574
  if (dir) {
15385
- return path11.resolve(cwd, dir);
15575
+ return path12.resolve(cwd, dir);
15386
15576
  }
15387
15577
  return await configExists(cwd) ? cwd : void 0;
15388
15578
  }
@@ -15399,7 +15589,7 @@ async function workspaceFor(client, requested, workflowDir) {
15399
15589
  return resolveWorkspace(client, void 0);
15400
15590
  }
15401
15591
  function registerIntegrations(program3) {
15402
- program3.command("integrations").description("List the workspace's connected integrations, whose ids go into integrationId inputs").option(
15592
+ program3.command("integrations").description("List the workspace's connected integrations").option(
15403
15593
  "--workspace <id>",
15404
15594
  "workspace to list (default: $SOFTR_WORKSPACE_ID, else the workflow directory's, else your only or default workspace)"
15405
15595
  ).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 +15599,7 @@ function registerIntegrations(program3) {
15409
15599
  const client = await authenticatedClient(target);
15410
15600
  const workspaceId = await workspaceFor(client, opts.workspace ?? process.env["SOFTR_WORKSPACE_ID"], workflowDir);
15411
15601
  const wanted = opts.type?.toUpperCase();
15412
- const rows = (await listWorkspaceIntegrations(client, workspaceId)).map(toRow2).filter((row) => !wanted || row.type === wanted).sort(byTypeThenName);
15602
+ const rows = (await listWorkspaceIntegrations(client, workspaceId)).map(toRow).filter((row) => !wanted || row.type === wanted).sort(byTypeThenName);
15413
15603
  emit(options, `Workspace ${workspaceId}${wanted ? `, type ${wanted}` : ""}:
15414
15604
  ${renderIntegrations(rows)}`, {
15415
15605
  workspaceId,
@@ -15419,6 +15609,105 @@ ${renderIntegrations(rows)}`, {
15419
15609
  });
15420
15610
  }
15421
15611
 
15612
+ // src/commands/list.ts
15613
+ var import_cli_table33 = __toESM(require_cli_table3(), 1);
15614
+ function toRow2(workflow, workspace) {
15615
+ return {
15616
+ id: workflow.id,
15617
+ title: workflow.title,
15618
+ workspaceId: workspace.id,
15619
+ workspaceName: workspace.name,
15620
+ live: workflow.enabledVersion ?? null,
15621
+ version: workflow.version,
15622
+ draft: workflow.draft,
15623
+ updatedAt: workflow.updatedAt ?? null,
15624
+ triggers: (workflow.triggerSummaries ?? []).map(({ id, type }) => ({ id, type }))
15625
+ };
15626
+ }
15627
+ async function collectWorkflows(client, { workspaceId, applicationId } = {}) {
15628
+ const workspaces = await listWorkspacesWith(client, WORKFLOWS_READ_SCOPE);
15629
+ if (workspaceId) {
15630
+ requireGrantedWorkspace(workspaces, workspaceId, WORKFLOWS_READ_SCOPE);
15631
+ }
15632
+ const selected = workspaceId ? workspaces.granted.filter((workspace) => workspace.id === workspaceId) : workspaces.granted;
15633
+ const listing = { workspaces: [], workflows: [], failures: [] };
15634
+ for (const workspace of selected) {
15635
+ try {
15636
+ const page = await listWorkflows(client, workspace.id, { applicationId });
15637
+ listing.workspaces.push({ id: workspace.id, name: workspace.name });
15638
+ listing.workflows.push(...page.items.map((workflow) => toRow2(workflow, workspace)));
15639
+ } catch (error) {
15640
+ if (!(error instanceof SoftrApiError) || selected.length === 1) {
15641
+ throw error;
15642
+ }
15643
+ listing.failures.push({
15644
+ workspaceId: workspace.id,
15645
+ workspaceName: workspace.name,
15646
+ status: error.status,
15647
+ message: error.body
15648
+ });
15649
+ }
15650
+ }
15651
+ return listing;
15652
+ }
15653
+ var TITLE_WIDTH = 34;
15654
+ function workflowTable(rows) {
15655
+ const table = new import_cli_table33.default({
15656
+ head: ["ID", "Title", "Triggers", "Status", "Version", "Draft"],
15657
+ // Only the free-text title wraps; the other columns size to their content, so trigger types are never cut off.
15658
+ colWidths: [null, TITLE_WIDTH, null, null, null, null],
15659
+ wordWrap: true,
15660
+ style: { head: [], border: [], "padding-left": 1, "padding-right": 1 }
15661
+ });
15662
+ for (const row of rows) {
15663
+ table.push([
15664
+ row.id,
15665
+ row.title,
15666
+ row.triggers.map((trigger) => trigger.type).join("\n") || "-",
15667
+ row.live === null ? "not enabled" : `live v${row.live}`,
15668
+ row.version,
15669
+ row.draft
15670
+ ]);
15671
+ }
15672
+ return table.toString();
15673
+ }
15674
+ function renderWorkflows({ workspaces, workflows: workflows2, failures }) {
15675
+ const blocks = [];
15676
+ for (const workspace of workspaces) {
15677
+ const rows = workflows2.filter((row) => row.workspaceId === workspace.id);
15678
+ const heading = `Workspace: ${workspace.name} (${workspace.id})`;
15679
+ blocks.push(rows.length ? `${heading}
15680
+ ${workflowTable(rows)}` : `${heading}
15681
+ (no workflows)`);
15682
+ }
15683
+ if (failures.length) {
15684
+ blocks.push(
15685
+ [
15686
+ `Listing failed for ${failures.length} workspace${failures.length === 1 ? "" : "s"} (skipped):`,
15687
+ ...failures.map(
15688
+ (failure) => ` ${failure.workspaceName} (${failure.workspaceId}): status ${failure.status}${failure.message ? `: ${failure.message}` : ""}`
15689
+ )
15690
+ ].join("\n")
15691
+ );
15692
+ }
15693
+ return blocks.join("\n\n");
15694
+ }
15695
+ async function emitWorkflowList(options, client, { workspaceId, applicationId, human } = {}) {
15696
+ const listing = await collectWorkflows(client, { workspaceId, applicationId });
15697
+ emit(options, [human, renderWorkflows(listing)].filter(Boolean).join("\n\n"), { ...listing });
15698
+ }
15699
+ function registerList(program3) {
15700
+ 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) => {
15701
+ const options = globalOptions(command);
15702
+ const target = resolveTarget(await readState(process.cwd()));
15703
+ const client = await authenticatedClient(target);
15704
+ await emitWorkflowList(options, client, {
15705
+ workspaceId: opts.workspace ?? process.env["SOFTR_WORKSPACE_ID"],
15706
+ applicationId: opts.app
15707
+ });
15708
+ });
15709
+ }
15710
+
15422
15711
  // src/commands/login.ts
15423
15712
  import { execFile } from "node:child_process";
15424
15713
  import { createServer } from "node:http";
@@ -15431,10 +15720,10 @@ async function identify(client) {
15431
15720
  return { userId: owner.id, email: owner.email, statements: owner.statements };
15432
15721
  }
15433
15722
  function scopeWarning(identity) {
15434
- if (grantsWorkflows(identity.statements)) {
15723
+ if (grantsWorkflows(identity.statements) || grantsDatabases(identity.statements)) {
15435
15724
  return "";
15436
15725
  }
15437
- return "\nWarning: this token grants no workflows action (workflows:read, workflows:write, workflows:execute); most commands will be refused.";
15726
+ 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
15727
  }
15439
15728
  async function interactiveLogin(target) {
15440
15729
  if (!stdin.isTTY) {
@@ -15684,7 +15973,7 @@ ${identity.statements.map((s) => ` ${s.actions.join(", ")} on ${s.resources.joi
15684
15973
  }
15685
15974
 
15686
15975
  // src/commands/outputs.ts
15687
- var import_cli_table34 = __toESM(require_cli_table3(), 1);
15976
+ var import_cli_table35 = __toESM(require_cli_table3(), 1);
15688
15977
 
15689
15978
  // src/project/test-state.ts
15690
15979
  function outdatedReferences(row) {
@@ -15697,28 +15986,28 @@ function renderIssues(issues) {
15697
15986
 
15698
15987
  // src/project/variables.ts
15699
15988
  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, "\\'")}']`;
15989
+ function appendKey(path15, key) {
15990
+ return /^[A-Za-z_]\w*$/.test(key) ? `${path15}.${key}` : `${path15}['${key.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}']`;
15702
15991
  }
15703
15992
  function isPrimitive2(value) {
15704
15993
  return value !== void 0 && value !== null && typeof value !== "object";
15705
15994
  }
15706
- function walk(type, raw, path13, token, out) {
15995
+ function walk(type, raw, path15, token, out) {
15707
15996
  if (out.length >= MAX_VARIABLES) {
15708
15997
  return;
15709
15998
  }
15710
- out.push({ variable: token(path13), path: path13, type, ...isPrimitive2(raw) ? { example: raw } : {} });
15999
+ out.push({ variable: token(path15), path: path15, type, ...isPrimitive2(raw) ? { example: raw } : {} });
15711
16000
  if (type.kind === "object") {
15712
16001
  for (const [key, field] of Object.entries(type.fields)) {
15713
16002
  const rawField = raw !== null && typeof raw === "object" && !Array.isArray(raw) ? raw[key] : void 0;
15714
- walk(field.type, rawField, appendKey(path13, key), token, out);
16003
+ walk(field.type, rawField, appendKey(path15, key), token, out);
15715
16004
  }
15716
16005
  } else if (type.kind === "array") {
15717
- walk(type.items, Array.isArray(raw) ? raw[0] : void 0, `${path13}[*]`, token, out);
16006
+ walk(type.items, Array.isArray(raw) ? raw[0] : void 0, `${path15}[*]`, token, out);
15718
16007
  }
15719
16008
  }
15720
16009
  function collectVariables(nodeId, sample, metadata) {
15721
- const token = (path13) => `{outputs.${nodeId}:::${path13}}`;
16010
+ const token = (path15) => `{outputs.${nodeId}:::${path15}}`;
15722
16011
  const variables = [];
15723
16012
  if (sample !== void 0 && sample !== null) {
15724
16013
  walk(typeOfJson(sample), sample, "$", token, variables);
@@ -15733,7 +16022,7 @@ function collectVariables(nodeId, sample, metadata) {
15733
16022
  }
15734
16023
 
15735
16024
  // src/commands/test.ts
15736
- var import_cli_table33 = __toESM(require_cli_table3(), 1);
16025
+ var import_cli_table34 = __toESM(require_cli_table3(), 1);
15737
16026
  var OUTPUTS_FILE = "outputs.json";
15738
16027
  function fieldCount(row) {
15739
16028
  const output = row.sampleOutput?.result?.output;
@@ -15834,7 +16123,7 @@ var LABEL = {
15834
16123
  error: "ERROR"
15835
16124
  };
15836
16125
  function renderResults(results) {
15837
- const table = new import_cli_table33.default({
16126
+ const table = new import_cli_table34.default({
15838
16127
  head: ["Result", "Node", "Type", "Detail"],
15839
16128
  colWidths: [9, 30, 24, 60],
15840
16129
  wordWrap: true,
@@ -15950,7 +16239,7 @@ function sampleState(row) {
15950
16239
  }
15951
16240
  }
15952
16241
  function renderVariables(nodeId, entries, unverified) {
15953
- const table = new import_cli_table34.default({
16242
+ const table = new import_cli_table35.default({
15954
16243
  head: ["Variable", "Type", "Example"],
15955
16244
  colWidths: [64, 32, 44],
15956
16245
  wordWrap: true,
@@ -16018,7 +16307,7 @@ function registerOutputs(program3) {
16018
16307
  outdated: Object.keys(outdatedReferences(row))
16019
16308
  };
16020
16309
  });
16021
- const table = new import_cli_table34.default({
16310
+ const table = new import_cli_table35.default({
16022
16311
  head: ["Node", "Type", "Sample", "Fields"],
16023
16312
  colWidths: [30, 24, 60, 8],
16024
16313
  wordWrap: true,
@@ -16050,19 +16339,19 @@ function pathFromKeys(keys) {
16050
16339
  const only = keys[0].trim();
16051
16340
  return only.startsWith("$") ? only : `$${only.startsWith("[") ? "" : "."}${only}`;
16052
16341
  }
16053
- let path13 = "$";
16342
+ let path15 = "$";
16054
16343
  for (const key of keys) {
16055
16344
  if (key === "*") {
16056
- path13 += "[*]";
16345
+ path15 += "[*]";
16057
16346
  } else if (/^\d+$/.test(key)) {
16058
- path13 += `[${key}]`;
16347
+ path15 += `[${key}]`;
16059
16348
  } else if (/^[A-Za-z_]\w*$/.test(key)) {
16060
- path13 += `.${key}`;
16349
+ path15 += `.${key}`;
16061
16350
  } else {
16062
- path13 += `['${key.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}']`;
16351
+ path15 += `['${key.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}']`;
16063
16352
  }
16064
16353
  }
16065
- return path13;
16354
+ return path15;
16066
16355
  }
16067
16356
  function keysAt(type) {
16068
16357
  switch (type.kind) {
@@ -16087,12 +16376,12 @@ function registerPlaceholder(program3) {
16087
16376
  }
16088
16377
  const rows = await readStateFile(dir, OUTPUTS_FILE) ?? [];
16089
16378
  const outputs = indexOutputs(rows);
16090
- const path13 = pathFromKeys(keys);
16091
- if (!parsePath(path13)) {
16092
- throw new CliError(`Cannot read the path "${path13}".`);
16379
+ const path15 = pathFromKeys(keys);
16380
+ if (!parsePath(path15)) {
16381
+ throw new CliError(`Cannot read the path "${path15}".`);
16093
16382
  }
16094
16383
  const isLoop = location.node.type === LOOP_TYPE;
16095
- const token = isLoop ? `{loopActionGroup.${nodeId}:::loopVariables.items${path13 === "$" ? "" : path13.slice(1)}}` : `{outputs.${nodeId}:::${path13}}`;
16384
+ const token = isLoop ? `{loopActionGroup.${nodeId}:::loopVariables.items${path15 === "$" ? "" : path15.slice(1)}}` : `{outputs.${nodeId}:::${path15}}`;
16096
16385
  const [description] = describeInputData(
16097
16386
  { id: "probe", type: "CUSTOM_CODE", inputs: { inputData: { probe: token } } },
16098
16387
  config,
@@ -16102,7 +16391,7 @@ function registerPlaceholder(program3) {
16102
16391
  throw new CliError("Could not evaluate the token.");
16103
16392
  }
16104
16393
  if (description.type.kind === "unknown") {
16105
- const segments = parsePath(path13) ?? [];
16394
+ const segments = parsePath(path15) ?? [];
16106
16395
  let hint = description.note ?? "not found";
16107
16396
  const row = outputs[nodeId];
16108
16397
  if (row?.hasSample && segments.length) {
@@ -16128,7 +16417,7 @@ function registerPlaceholder(program3) {
16128
16417
  emit(options, `${token}
16129
16418
  type: ${type}${example3} (${status})`, {
16130
16419
  token,
16131
- path: path13,
16420
+ path: path15,
16132
16421
  type,
16133
16422
  example: description.example ?? null,
16134
16423
  verified: description.verified,
@@ -16137,6 +16426,84 @@ type: ${type}${example3} (${status})`, {
16137
16426
  });
16138
16427
  }
16139
16428
 
16429
+ // src/commands/pull.ts
16430
+ import path13 from "node:path";
16431
+ async function targetDir(cwd, explicitDir, workflowId, title) {
16432
+ if (explicitDir) {
16433
+ return path13.resolve(cwd, explicitDir);
16434
+ }
16435
+ if (await configExists(cwd)) {
16436
+ const existing = await readConfig(cwd);
16437
+ if (existing.id && existing.id !== workflowId) {
16438
+ throw new CliError(
16439
+ `${CONFIG_FILE_NAME} here belongs to workflow ${existing.id}, not ${workflowId}.`,
16440
+ "Run pull from the parent directory (a new subdirectory is created) or pass --dir."
16441
+ );
16442
+ }
16443
+ return cwd;
16444
+ }
16445
+ return path13.join(cwd, slug(title));
16446
+ }
16447
+ function registerPull(program3) {
16448
+ 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) => {
16449
+ const options = globalOptions(command);
16450
+ const cwd = process.cwd();
16451
+ const remembered = await readState(opts.dir ? path13.resolve(cwd, opts.dir) : cwd);
16452
+ const target = resolveTarget(remembered);
16453
+ const client = await authenticatedClient(target);
16454
+ if (!workflowId) {
16455
+ await emitWorkflowList(options, client, {
16456
+ human: "Pick a workflow and run: softr workflows pull <workflowId>"
16457
+ });
16458
+ return;
16459
+ }
16460
+ const workflow = await getWorkflow(client, workflowId);
16461
+ const dir = await targetDir(cwd, opts.dir, workflowId, workflow.title);
16462
+ if (await configExists(dir) && !opts.force) {
16463
+ throw new CliError(
16464
+ `${path13.join(dir, CONFIG_FILE_NAME)} already exists.`,
16465
+ "Pass --force to overwrite it and its code files."
16466
+ );
16467
+ }
16468
+ let rows = [];
16469
+ let outputsNote = "";
16470
+ let outputsFile;
16471
+ try {
16472
+ rows = await getWorkflowOutputs(client, workflowId);
16473
+ } catch (error) {
16474
+ outputsNote = `
16475
+ Sample outputs not available, inputData types are unknown: ${error.message}`;
16476
+ }
16477
+ const config = { $schema: SCHEMA_POINTER, ...toLocalConfig(workflow) };
16478
+ const codeFiles = await externalizeCode(config, dir, indexOutputs(rows));
16479
+ await writeSchemaFile(dir);
16480
+ const configFile2 = await writeConfig(dir, config);
16481
+ const syncedHash = definitionHash((await packWorkflow(config, dir)).packed);
16482
+ await writeState(dir, { ...target, workflowId, version: workflow.version, draft: workflow.draft, syncedHash });
16483
+ if (!outputsNote) {
16484
+ outputsFile = await writeStateFile(dir, "outputs.json", rows);
16485
+ }
16486
+ const files = [configFile2, ...codeFiles].map((file) => path13.relative(cwd, file));
16487
+ emitWithVersionNote(
16488
+ options,
16489
+ config,
16490
+ `Pulled "${workflow.title}" (version ${workflow.version}, draft ${workflow.draft}, ${workflow.enabled ? "enabled" : "not enabled"}).
16491
+ ` + files.map((file) => ` ${file}`).join("\n") + outputsNote,
16492
+ {
16493
+ workflowId,
16494
+ apiUrl: target.apiUrl,
16495
+ title: workflow.title,
16496
+ version: workflow.version,
16497
+ draft: workflow.draft,
16498
+ enabled: workflow.enabled ?? false,
16499
+ dir,
16500
+ files,
16501
+ outputsFile: outputsFile ?? null
16502
+ }
16503
+ );
16504
+ });
16505
+ }
16506
+
16140
16507
  // src/commands/publish.ts
16141
16508
  import { stdin as stdin2, stdout } from "node:process";
16142
16509
  import { createInterface as createInterface2 } from "node:readline/promises";
@@ -16374,23 +16741,23 @@ function registerPin(program3) {
16374
16741
  }
16375
16742
 
16376
16743
  // src/commands/push.ts
16377
- import path12 from "node:path";
16744
+ import path14 from "node:path";
16378
16745
 
16379
16746
  // src/project/placeholder-check.ts
16380
16747
  var TOKEN = /\{(?:outputs|loopActionGroup)\.[^:{}]+:::[^{}]*\}/g;
16381
- function* stringsIn(value, path13) {
16748
+ function* stringsIn(value, path15) {
16382
16749
  if (typeof value === "string") {
16383
- yield [path13, value];
16750
+ yield [path15, value];
16384
16751
  } else if (Array.isArray(value)) {
16385
16752
  for (const [index, item] of value.entries()) {
16386
- yield* stringsIn(item, `${path13}[${index}]`);
16753
+ yield* stringsIn(item, `${path15}[${index}]`);
16387
16754
  }
16388
16755
  } else if (value !== null && typeof value === "object") {
16389
16756
  for (const [key, item] of Object.entries(value)) {
16390
16757
  if (key === "actions" || key === "paths") {
16391
16758
  continue;
16392
16759
  }
16393
- yield* stringsIn(item, path13 ? `${path13}.${key}` : key);
16760
+ yield* stringsIn(item, path15 ? `${path15}.${key}` : key);
16394
16761
  }
16395
16762
  }
16396
16763
  }
@@ -16501,7 +16868,7 @@ ${unbacked.map(
16501
16868
  draft: result.draft,
16502
16869
  inlined,
16503
16870
  studioUrl,
16504
- dir: path12.resolve(dir),
16871
+ dir: path14.resolve(dir),
16505
16872
  unbackedPlaceholders: unbacked
16506
16873
  }
16507
16874
  );
@@ -16509,10 +16876,10 @@ ${unbacked.map(
16509
16876
  }
16510
16877
 
16511
16878
  // src/commands/spec.ts
16512
- var import_cli_table35 = __toESM(require_cli_table3(), 1);
16879
+ var import_cli_table36 = __toESM(require_cli_table3(), 1);
16513
16880
  var TABLE_STYLE2 = { head: [], border: [], "padding-left": 1, "padding-right": 1 };
16514
16881
  function renderList() {
16515
- const table = new import_cli_table35.default({
16882
+ const table = new import_cli_table36.default({
16516
16883
  head: ["Type", "Kind", "Label", "Summary"],
16517
16884
  colWidths: [40, 9, 24, 70],
16518
16885
  wordWrap: true,
@@ -16540,7 +16907,7 @@ function renderEntry(entry) {
16540
16907
  const blocks = [`${entry.type} (${entry.kind}) \u2014 ${entry.label}`, entry.summary, `Docs: ${entry.docs}`];
16541
16908
  const inputs = Object.entries(entry.inputs);
16542
16909
  if (inputs.length) {
16543
- const table = new import_cli_table35.default({
16910
+ const table = new import_cli_table36.default({
16544
16911
  head: ["Input", "Type", "Required", "Description"],
16545
16912
  colWidths: [20, 9, 10, 90],
16546
16913
  wordWrap: true,
@@ -16790,21 +17157,27 @@ ${item.lines.map((line) => ` ${line}`).join("\n")}`
16790
17157
  }
16791
17158
 
16792
17159
  // src/commands/workspaces.ts
16793
- var import_cli_table36 = __toESM(require_cli_table3(), 1);
17160
+ var import_cli_table37 = __toESM(require_cli_table3(), 1);
16794
17161
  function registerWorkspaces(program3) {
16795
- program3.command("workspaces").description("The workspaces the stored token reaches, with your role in each").action(async (_opts, command) => {
17162
+ 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
17163
  const options = globalOptions(command);
16797
17164
  const target = resolveTarget();
16798
17165
  const client = await authenticatedClient(target);
16799
17166
  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],
17167
+ const table = new import_cli_table37.default({
17168
+ head: ["ID", "Name", "Role", "Default", "Scopes"],
17169
+ colWidths: [38, 26, 10, 9, 34],
16803
17170
  wordWrap: true,
16804
17171
  style: { head: [], border: [], "padding-left": 1, "padding-right": 1 }
16805
17172
  });
16806
17173
  for (const workspace of workspaces) {
16807
- table.push([workspace.id, workspace.name, workspace.userRole ?? "", workspace.isDefault ? "yes" : ""]);
17174
+ table.push([
17175
+ workspace.id,
17176
+ workspace.name,
17177
+ workspace.userRole ?? "",
17178
+ workspace.isDefault ? "yes" : "",
17179
+ (workspace.scopes ?? []).join(", ")
17180
+ ]);
16808
17181
  }
16809
17182
  emit(
16810
17183
  options,
@@ -16818,11 +17191,12 @@ Pass --workspace <id> to a command, or set SOFTR_WORKSPACE_ID, to work in one of
16818
17191
  // package.json
16819
17192
  var package_default = {
16820
17193
  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",
17194
+ version: "0.1.4",
17195
+ description: "The Softr command line: build, test and publish Softr workflows from local files, manage Softr Databases; more Softr APIs to come",
16823
17196
  keywords: [
16824
17197
  "softr",
16825
17198
  "workflows",
17199
+ "databases",
16826
17200
  "cli"
16827
17201
  ],
16828
17202
  license: "UNLICENSED",
@@ -16868,7 +17242,7 @@ var package_default = {
16868
17242
  };
16869
17243
 
16870
17244
  // 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");
17245
+ 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
17246
  registerLogin(program2);
16873
17247
  registerWorkspaces(program2);
16874
17248
  registerIntegrations(program2);
@@ -16888,4 +17262,6 @@ registerPublish(workflows);
16888
17262
  registerPin(workflows);
16889
17263
  registerSpec(workflows);
16890
17264
  registerSchema(workflows);
17265
+ var db = program2.command("db").description("Manage Softr Databases: databases, their tables, fields and records");
17266
+ registerDatabases(db);
16891
17267
  program2.parseAsync(process.argv).catch(reportError);