@softr/cli 0.1.2 → 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.
- package/README.md +79 -34
- package/dist/cli.mjs +953 -525
- 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
|
|
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
|
-
|
|
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 =
|
|
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(
|
|
4955
|
-
if (
|
|
4956
|
-
this._executableDir =
|
|
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
|
|
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
|
|
5314
|
-
const to =
|
|
5315
|
-
if (!to || !nodes.has(
|
|
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(
|
|
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
|
|
6111
|
-
|
|
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
|
-
|
|
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
|
|
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} ${
|
|
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,
|
|
6136
|
-
const response = await send(client, "workflows", method,
|
|
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
|
|
6140
|
-
await send(client, "
|
|
6165
|
+
async function tablesRequestVoid(client, method, path15, body) {
|
|
6166
|
+
await send(client, "tables", method, path15, body);
|
|
6141
6167
|
}
|
|
6142
|
-
async function studioRequest(client, method,
|
|
6143
|
-
const response = await send(client, "studio", method,
|
|
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,
|
|
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
|
|
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
|
|
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 (
|
|
7331
|
-
lastSegment =
|
|
7332
|
-
parent = findNodeAtLocation(root,
|
|
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,
|
|
7556
|
-
return setProperty(text,
|
|
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
|
|
7612
|
+
async function fileExists(file) {
|
|
7587
7613
|
try {
|
|
7588
|
-
await access(
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
13559
|
-
let rest =
|
|
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,
|
|
14060
|
-
const segments = parsePath(
|
|
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 "${
|
|
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/
|
|
14388
|
-
|
|
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
|
-
|
|
14408
|
-
|
|
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
|
|
14411
|
-
|
|
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
|
|
14414
|
-
|
|
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
|
-
|
|
14417
|
-
|
|
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
|
|
14420
|
-
return
|
|
14559
|
+
async function getDatabase(client, databaseId) {
|
|
14560
|
+
return (await tablesRequest(client, "GET", `${DATABASES}/${encodeURIComponent(databaseId)}`)).data;
|
|
14421
14561
|
}
|
|
14422
|
-
function
|
|
14423
|
-
return
|
|
14562
|
+
async function createDatabase(client, database) {
|
|
14563
|
+
return (await tablesRequest(client, "POST", DATABASES, database)).data;
|
|
14424
14564
|
}
|
|
14425
|
-
function
|
|
14426
|
-
return
|
|
14565
|
+
async function updateDatabase(client, databaseId, changes) {
|
|
14566
|
+
return (await tablesRequest(client, "PUT", `${DATABASES}/${encodeURIComponent(databaseId)}`, changes)).data;
|
|
14427
14567
|
}
|
|
14428
|
-
async function
|
|
14429
|
-
|
|
14430
|
-
|
|
14431
|
-
|
|
14432
|
-
|
|
14433
|
-
|
|
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
|
|
14439
|
-
return
|
|
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
|
|
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
|
|
14479
|
-
const response = await fetch(`${target.studioApiUrl}${
|
|
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 ${
|
|
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
|
|
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(
|
|
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/
|
|
14654
|
-
|
|
14655
|
-
|
|
14656
|
-
|
|
14657
|
-
|
|
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
|
-
|
|
14812
|
+
text = await readFile6(file, "utf8");
|
|
14660
14813
|
} catch (error) {
|
|
14661
|
-
if (error
|
|
14662
|
-
|
|
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
|
|
14671
|
-
|
|
14672
|
-
|
|
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
|
|
14830
|
+
async function readSchemaCache(dir) {
|
|
14696
14831
|
try {
|
|
14697
|
-
return JSON.parse(await readFile6(
|
|
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
|
|
14706
|
-
|
|
14707
|
-
|
|
14708
|
-
await writeFile6(
|
|
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
|
|
14711
|
-
|
|
14712
|
-
|
|
14713
|
-
|
|
14714
|
-
|
|
14715
|
-
|
|
14716
|
-
|
|
14717
|
-
|
|
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.');
|
|
14718
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 };
|
|
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
|
-
|
|
14721
|
-
|
|
14722
|
-
|
|
14723
|
-
|
|
14724
|
-
|
|
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
|
-
|
|
14728
|
-
|
|
14729
|
-
|
|
14730
|
-
|
|
14731
|
-
|
|
14732
|
-
|
|
14733
|
-
|
|
14734
|
-
|
|
14735
|
-
|
|
14736
|
-
|
|
14737
|
-
|
|
14738
|
-
|
|
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
|
|
14742
|
-
const
|
|
14743
|
-
const
|
|
14744
|
-
if (
|
|
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
|
-
`
|
|
14747
|
-
`
|
|
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
|
-
|
|
14751
|
-
|
|
14752
|
-
|
|
14753
|
-
|
|
14754
|
-
|
|
14755
|
-
|
|
14756
|
-
|
|
14757
|
-
|
|
14758
|
-
|
|
14759
|
-
|
|
14760
|
-
|
|
14761
|
-
|
|
14762
|
-
|
|
14763
|
-
|
|
14764
|
-
|
|
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
|
-
|
|
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
|
-
|
|
14771
|
-
|
|
14772
|
-
|
|
14773
|
-
|
|
14774
|
-
|
|
14775
|
-
|
|
14776
|
-
|
|
14777
|
-
|
|
14778
|
-
|
|
14779
|
-
|
|
14780
|
-
|
|
14781
|
-
|
|
14782
|
-
|
|
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
|
-
|
|
14792
|
-
|
|
14793
|
-
|
|
14794
|
-
|
|
14795
|
-
|
|
14796
|
-
|
|
14797
|
-
|
|
14798
|
-
|
|
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
|
-
|
|
14801
|
-
|
|
14802
|
-
|
|
14803
|
-
|
|
14804
|
-
|
|
14805
|
-
|
|
14806
|
-
|
|
14807
|
-
|
|
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
|
|
14813
|
-
|
|
14814
|
-
|
|
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
|
|
14817
|
-
|
|
14818
|
-
|
|
14819
|
-
|
|
14820
|
-
|
|
14821
|
-
|
|
14822
|
-
|
|
14823
|
-
|
|
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
|
|
@@ -15056,226 +15314,39 @@ function registerSchema(program3) {
|
|
|
15056
15314
|
});
|
|
15057
15315
|
}
|
|
15058
15316
|
|
|
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
|
-
);
|
|
15259
|
-
});
|
|
15260
|
-
}
|
|
15261
|
-
|
|
15262
15317
|
// src/commands/init.ts
|
|
15263
|
-
import path10 from "node:path";
|
|
15264
15318
|
var TRIGGER_ID = "trigger";
|
|
15265
|
-
|
|
15266
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
|
15354
|
-
function
|
|
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
|
|
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
|
|
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(
|
|
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,10 +15609,110 @@ ${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";
|
|
15425
15714
|
import { stderr, stdin } from "node:process";
|
|
15715
|
+
import { emitKeypressEvents } from "node:readline";
|
|
15426
15716
|
import { createInterface } from "node:readline/promises";
|
|
15427
15717
|
var CALLBACK_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
15428
15718
|
async function identify(client) {
|
|
@@ -15430,19 +15720,20 @@ async function identify(client) {
|
|
|
15430
15720
|
return { userId: owner.id, email: owner.email, statements: owner.statements };
|
|
15431
15721
|
}
|
|
15432
15722
|
function scopeWarning(identity) {
|
|
15433
|
-
if (grantsWorkflows(identity.statements)) {
|
|
15723
|
+
if (grantsWorkflows(identity.statements) || grantsDatabases(identity.statements)) {
|
|
15434
15724
|
return "";
|
|
15435
15725
|
}
|
|
15436
|
-
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.";
|
|
15437
15727
|
}
|
|
15438
15728
|
async function interactiveLogin(target) {
|
|
15439
15729
|
if (!stdin.isTTY) {
|
|
15440
15730
|
throw new CliError(
|
|
15441
15731
|
"No login method given and stdin is not a terminal.",
|
|
15442
|
-
`
|
|
15732
|
+
`Pipe a personal API token to "login --token", set ${TOKEN_ENV_VAR}, or run "login --no-browser" and open the printed URL.`
|
|
15443
15733
|
);
|
|
15444
15734
|
}
|
|
15445
15735
|
const rl = createInterface({ input: stdin, output: stderr });
|
|
15736
|
+
let answer = "";
|
|
15446
15737
|
try {
|
|
15447
15738
|
stderr.write(
|
|
15448
15739
|
`How do you want to log in to ${target.studioUrl}?
|
|
@@ -15450,7 +15741,6 @@ async function interactiveLogin(target) {
|
|
|
15450
15741
|
2. Personal API token: paste a pat_... created in the studio under API tokens
|
|
15451
15742
|
`
|
|
15452
15743
|
);
|
|
15453
|
-
let answer = "";
|
|
15454
15744
|
for (; ; ) {
|
|
15455
15745
|
answer = (await rl.question("Choose 1 or 2 [1]: ")).trim();
|
|
15456
15746
|
if (answer === "" || answer === "1" || answer === "2") {
|
|
@@ -15458,17 +15748,59 @@ async function interactiveLogin(target) {
|
|
|
15458
15748
|
}
|
|
15459
15749
|
stderr.write("Please answer 1 or 2.\n");
|
|
15460
15750
|
}
|
|
15461
|
-
if (answer === "2") {
|
|
15462
|
-
const token = (await rl.question("Personal API token: ")).trim();
|
|
15463
|
-
if (!token) {
|
|
15464
|
-
throw new CliError("No token given.", 'Run "softr login" again.');
|
|
15465
|
-
}
|
|
15466
|
-
return pastedToken(token);
|
|
15467
|
-
}
|
|
15468
15751
|
} finally {
|
|
15469
15752
|
rl.close();
|
|
15470
15753
|
}
|
|
15471
|
-
return browserLogin(target, false);
|
|
15754
|
+
return answer === "2" ? pastedToken(await promptToken()) : browserLogin(target, false);
|
|
15755
|
+
}
|
|
15756
|
+
function promptToken() {
|
|
15757
|
+
stderr.write("Personal API token: ");
|
|
15758
|
+
emitKeypressEvents(stdin);
|
|
15759
|
+
stdin.setRawMode(true);
|
|
15760
|
+
stdin.resume();
|
|
15761
|
+
return new Promise((resolve, reject) => {
|
|
15762
|
+
let token = "";
|
|
15763
|
+
const finish = (error) => {
|
|
15764
|
+
stdin.off("keypress", onKey);
|
|
15765
|
+
stdin.setRawMode(false);
|
|
15766
|
+
stdin.pause();
|
|
15767
|
+
stderr.write("\n");
|
|
15768
|
+
if (error) {
|
|
15769
|
+
reject(error);
|
|
15770
|
+
} else if (token.trim()) {
|
|
15771
|
+
resolve(token.trim());
|
|
15772
|
+
} else {
|
|
15773
|
+
reject(new CliError("No token given.", 'Run "softr login" again.'));
|
|
15774
|
+
}
|
|
15775
|
+
};
|
|
15776
|
+
const onKey = (text, key = {}) => {
|
|
15777
|
+
if (key.ctrl && (key.name === "c" || key.name === "d")) {
|
|
15778
|
+
finish(new CliError("Login cancelled."));
|
|
15779
|
+
} else if (key.name === "return" || key.name === "enter") {
|
|
15780
|
+
finish();
|
|
15781
|
+
} else if (key.name === "backspace") {
|
|
15782
|
+
if (token) {
|
|
15783
|
+
token = token.slice(0, -1);
|
|
15784
|
+
stderr.write("\b \b");
|
|
15785
|
+
}
|
|
15786
|
+
} else if (text && !key.ctrl && !key.meta && text >= " ") {
|
|
15787
|
+
token += text;
|
|
15788
|
+
stderr.write("*".repeat(text.length));
|
|
15789
|
+
}
|
|
15790
|
+
};
|
|
15791
|
+
stdin.on("keypress", onKey);
|
|
15792
|
+
});
|
|
15793
|
+
}
|
|
15794
|
+
async function readTokenFromStdin() {
|
|
15795
|
+
let input = "";
|
|
15796
|
+
for await (const chunk of stdin) {
|
|
15797
|
+
input += chunk;
|
|
15798
|
+
}
|
|
15799
|
+
const token = input.trim();
|
|
15800
|
+
if (!token) {
|
|
15801
|
+
throw new CliError("No token on stdin.", 'Pipe the token in, for example "pbpaste | softr login --token".');
|
|
15802
|
+
}
|
|
15803
|
+
return token;
|
|
15472
15804
|
}
|
|
15473
15805
|
function openBrowser(url) {
|
|
15474
15806
|
const [command, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["rundll32", ["url.dll,FileProtocolHandler", url]] : ["xdg-open", [url]];
|
|
@@ -15568,17 +15900,26 @@ function registerLogin(program3) {
|
|
|
15568
15900
|
program3.command("login").description(
|
|
15569
15901
|
"Log in through the browser (recommended) or with a personal API token; asks which when run in a terminal"
|
|
15570
15902
|
).option(
|
|
15571
|
-
"--token
|
|
15572
|
-
`a personal API token (pat_...) from the studio,
|
|
15573
|
-
).option("--no-browser", "browser login, but print the URL instead of opening it (for remote shells and agents)").action(async (opts, command) => {
|
|
15903
|
+
"--token",
|
|
15904
|
+
`log in with a personal API token (pat_...) from the studio: read from stdin when piped, else asked for with the input hidden; without the flag, $${TOKEN_ENV_VAR} is used when set`
|
|
15905
|
+
).option("--no-browser", "browser login, but print the URL instead of opening it (for remote shells and agents)").allowExcessArguments().allowUnknownOption().action(async (opts, command) => {
|
|
15574
15906
|
const options = globalOptions(command);
|
|
15575
15907
|
const target = resolveTarget();
|
|
15908
|
+
if (command.args.length > 0) {
|
|
15909
|
+
throw opts.token || command.args.some((arg) => arg.startsWith("--token=")) ? new CliError(
|
|
15910
|
+
'"--token" takes no value any more: a token on the command line ends up in the shell history.',
|
|
15911
|
+
'Pipe it in ("pbpaste | softr login --token") or run "softr login --token" and paste it at the prompt. Revoke the token you passed in the studio (API tokens) and create a new one.'
|
|
15912
|
+
) : new CliError(
|
|
15913
|
+
'"login" takes no arguments and knows only the options its help lists.',
|
|
15914
|
+
'Run "softr login --help".'
|
|
15915
|
+
);
|
|
15916
|
+
}
|
|
15576
15917
|
if (isProxyAuth()) {
|
|
15577
15918
|
throw new CliError(
|
|
15578
15919
|
`Login is not needed: the target authenticates the requests itself ($${AUTH_ENV_VAR}=proxy).`
|
|
15579
15920
|
);
|
|
15580
15921
|
}
|
|
15581
|
-
const token =
|
|
15922
|
+
const token = opts.token ? stdin.isTTY ? await promptToken() : await readTokenFromStdin() : (process.env[TOKEN_ENV_VAR] ?? "").trim();
|
|
15582
15923
|
const saved = token ? pastedToken(token) : opts.browser ? await interactiveLogin(target) : await browserLogin(target, true);
|
|
15583
15924
|
const identity = await identify({ target, token: saved.token, kind: saved.kind });
|
|
15584
15925
|
await report(options, target, saved, identity);
|
|
@@ -15632,7 +15973,7 @@ ${identity.statements.map((s) => ` ${s.actions.join(", ")} on ${s.resources.joi
|
|
|
15632
15973
|
}
|
|
15633
15974
|
|
|
15634
15975
|
// src/commands/outputs.ts
|
|
15635
|
-
var
|
|
15976
|
+
var import_cli_table35 = __toESM(require_cli_table3(), 1);
|
|
15636
15977
|
|
|
15637
15978
|
// src/project/test-state.ts
|
|
15638
15979
|
function outdatedReferences(row) {
|
|
@@ -15645,28 +15986,28 @@ function renderIssues(issues) {
|
|
|
15645
15986
|
|
|
15646
15987
|
// src/project/variables.ts
|
|
15647
15988
|
var MAX_VARIABLES = 200;
|
|
15648
|
-
function appendKey(
|
|
15649
|
-
return /^[A-Za-z_]\w*$/.test(key) ? `${
|
|
15989
|
+
function appendKey(path15, key) {
|
|
15990
|
+
return /^[A-Za-z_]\w*$/.test(key) ? `${path15}.${key}` : `${path15}['${key.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}']`;
|
|
15650
15991
|
}
|
|
15651
15992
|
function isPrimitive2(value) {
|
|
15652
15993
|
return value !== void 0 && value !== null && typeof value !== "object";
|
|
15653
15994
|
}
|
|
15654
|
-
function walk(type, raw,
|
|
15995
|
+
function walk(type, raw, path15, token, out) {
|
|
15655
15996
|
if (out.length >= MAX_VARIABLES) {
|
|
15656
15997
|
return;
|
|
15657
15998
|
}
|
|
15658
|
-
out.push({ variable: token(
|
|
15999
|
+
out.push({ variable: token(path15), path: path15, type, ...isPrimitive2(raw) ? { example: raw } : {} });
|
|
15659
16000
|
if (type.kind === "object") {
|
|
15660
16001
|
for (const [key, field] of Object.entries(type.fields)) {
|
|
15661
16002
|
const rawField = raw !== null && typeof raw === "object" && !Array.isArray(raw) ? raw[key] : void 0;
|
|
15662
|
-
walk(field.type, rawField, appendKey(
|
|
16003
|
+
walk(field.type, rawField, appendKey(path15, key), token, out);
|
|
15663
16004
|
}
|
|
15664
16005
|
} else if (type.kind === "array") {
|
|
15665
|
-
walk(type.items, Array.isArray(raw) ? raw[0] : void 0, `${
|
|
16006
|
+
walk(type.items, Array.isArray(raw) ? raw[0] : void 0, `${path15}[*]`, token, out);
|
|
15666
16007
|
}
|
|
15667
16008
|
}
|
|
15668
16009
|
function collectVariables(nodeId, sample, metadata) {
|
|
15669
|
-
const token = (
|
|
16010
|
+
const token = (path15) => `{outputs.${nodeId}:::${path15}}`;
|
|
15670
16011
|
const variables = [];
|
|
15671
16012
|
if (sample !== void 0 && sample !== null) {
|
|
15672
16013
|
walk(typeOfJson(sample), sample, "$", token, variables);
|
|
@@ -15681,7 +16022,7 @@ function collectVariables(nodeId, sample, metadata) {
|
|
|
15681
16022
|
}
|
|
15682
16023
|
|
|
15683
16024
|
// src/commands/test.ts
|
|
15684
|
-
var
|
|
16025
|
+
var import_cli_table34 = __toESM(require_cli_table3(), 1);
|
|
15685
16026
|
var OUTPUTS_FILE = "outputs.json";
|
|
15686
16027
|
function fieldCount(row) {
|
|
15687
16028
|
const output = row.sampleOutput?.result?.output;
|
|
@@ -15782,7 +16123,7 @@ var LABEL = {
|
|
|
15782
16123
|
error: "ERROR"
|
|
15783
16124
|
};
|
|
15784
16125
|
function renderResults(results) {
|
|
15785
|
-
const table = new
|
|
16126
|
+
const table = new import_cli_table34.default({
|
|
15786
16127
|
head: ["Result", "Node", "Type", "Detail"],
|
|
15787
16128
|
colWidths: [9, 30, 24, 60],
|
|
15788
16129
|
wordWrap: true,
|
|
@@ -15898,7 +16239,7 @@ function sampleState(row) {
|
|
|
15898
16239
|
}
|
|
15899
16240
|
}
|
|
15900
16241
|
function renderVariables(nodeId, entries, unverified) {
|
|
15901
|
-
const table = new
|
|
16242
|
+
const table = new import_cli_table35.default({
|
|
15902
16243
|
head: ["Variable", "Type", "Example"],
|
|
15903
16244
|
colWidths: [64, 32, 44],
|
|
15904
16245
|
wordWrap: true,
|
|
@@ -15966,7 +16307,7 @@ function registerOutputs(program3) {
|
|
|
15966
16307
|
outdated: Object.keys(outdatedReferences(row))
|
|
15967
16308
|
};
|
|
15968
16309
|
});
|
|
15969
|
-
const table = new
|
|
16310
|
+
const table = new import_cli_table35.default({
|
|
15970
16311
|
head: ["Node", "Type", "Sample", "Fields"],
|
|
15971
16312
|
colWidths: [30, 24, 60, 8],
|
|
15972
16313
|
wordWrap: true,
|
|
@@ -15998,19 +16339,19 @@ function pathFromKeys(keys) {
|
|
|
15998
16339
|
const only = keys[0].trim();
|
|
15999
16340
|
return only.startsWith("$") ? only : `$${only.startsWith("[") ? "" : "."}${only}`;
|
|
16000
16341
|
}
|
|
16001
|
-
let
|
|
16342
|
+
let path15 = "$";
|
|
16002
16343
|
for (const key of keys) {
|
|
16003
16344
|
if (key === "*") {
|
|
16004
|
-
|
|
16345
|
+
path15 += "[*]";
|
|
16005
16346
|
} else if (/^\d+$/.test(key)) {
|
|
16006
|
-
|
|
16347
|
+
path15 += `[${key}]`;
|
|
16007
16348
|
} else if (/^[A-Za-z_]\w*$/.test(key)) {
|
|
16008
|
-
|
|
16349
|
+
path15 += `.${key}`;
|
|
16009
16350
|
} else {
|
|
16010
|
-
|
|
16351
|
+
path15 += `['${key.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}']`;
|
|
16011
16352
|
}
|
|
16012
16353
|
}
|
|
16013
|
-
return
|
|
16354
|
+
return path15;
|
|
16014
16355
|
}
|
|
16015
16356
|
function keysAt(type) {
|
|
16016
16357
|
switch (type.kind) {
|
|
@@ -16035,12 +16376,12 @@ function registerPlaceholder(program3) {
|
|
|
16035
16376
|
}
|
|
16036
16377
|
const rows = await readStateFile(dir, OUTPUTS_FILE) ?? [];
|
|
16037
16378
|
const outputs = indexOutputs(rows);
|
|
16038
|
-
const
|
|
16039
|
-
if (!parsePath(
|
|
16040
|
-
throw new CliError(`Cannot read the path "${
|
|
16379
|
+
const path15 = pathFromKeys(keys);
|
|
16380
|
+
if (!parsePath(path15)) {
|
|
16381
|
+
throw new CliError(`Cannot read the path "${path15}".`);
|
|
16041
16382
|
}
|
|
16042
16383
|
const isLoop = location.node.type === LOOP_TYPE;
|
|
16043
|
-
const token = isLoop ? `{loopActionGroup.${nodeId}:::loopVariables.items${
|
|
16384
|
+
const token = isLoop ? `{loopActionGroup.${nodeId}:::loopVariables.items${path15 === "$" ? "" : path15.slice(1)}}` : `{outputs.${nodeId}:::${path15}}`;
|
|
16044
16385
|
const [description] = describeInputData(
|
|
16045
16386
|
{ id: "probe", type: "CUSTOM_CODE", inputs: { inputData: { probe: token } } },
|
|
16046
16387
|
config,
|
|
@@ -16050,7 +16391,7 @@ function registerPlaceholder(program3) {
|
|
|
16050
16391
|
throw new CliError("Could not evaluate the token.");
|
|
16051
16392
|
}
|
|
16052
16393
|
if (description.type.kind === "unknown") {
|
|
16053
|
-
const segments = parsePath(
|
|
16394
|
+
const segments = parsePath(path15) ?? [];
|
|
16054
16395
|
let hint = description.note ?? "not found";
|
|
16055
16396
|
const row = outputs[nodeId];
|
|
16056
16397
|
if (row?.hasSample && segments.length) {
|
|
@@ -16076,7 +16417,7 @@ function registerPlaceholder(program3) {
|
|
|
16076
16417
|
emit(options, `${token}
|
|
16077
16418
|
type: ${type}${example3} (${status})`, {
|
|
16078
16419
|
token,
|
|
16079
|
-
path:
|
|
16420
|
+
path: path15,
|
|
16080
16421
|
type,
|
|
16081
16422
|
example: description.example ?? null,
|
|
16082
16423
|
verified: description.verified,
|
|
@@ -16085,6 +16426,84 @@ type: ${type}${example3} (${status})`, {
|
|
|
16085
16426
|
});
|
|
16086
16427
|
}
|
|
16087
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
|
+
|
|
16088
16507
|
// src/commands/publish.ts
|
|
16089
16508
|
import { stdin as stdin2, stdout } from "node:process";
|
|
16090
16509
|
import { createInterface as createInterface2 } from "node:readline/promises";
|
|
@@ -16322,23 +16741,23 @@ function registerPin(program3) {
|
|
|
16322
16741
|
}
|
|
16323
16742
|
|
|
16324
16743
|
// src/commands/push.ts
|
|
16325
|
-
import
|
|
16744
|
+
import path14 from "node:path";
|
|
16326
16745
|
|
|
16327
16746
|
// src/project/placeholder-check.ts
|
|
16328
16747
|
var TOKEN = /\{(?:outputs|loopActionGroup)\.[^:{}]+:::[^{}]*\}/g;
|
|
16329
|
-
function* stringsIn(value,
|
|
16748
|
+
function* stringsIn(value, path15) {
|
|
16330
16749
|
if (typeof value === "string") {
|
|
16331
|
-
yield [
|
|
16750
|
+
yield [path15, value];
|
|
16332
16751
|
} else if (Array.isArray(value)) {
|
|
16333
16752
|
for (const [index, item] of value.entries()) {
|
|
16334
|
-
yield* stringsIn(item, `${
|
|
16753
|
+
yield* stringsIn(item, `${path15}[${index}]`);
|
|
16335
16754
|
}
|
|
16336
16755
|
} else if (value !== null && typeof value === "object") {
|
|
16337
16756
|
for (const [key, item] of Object.entries(value)) {
|
|
16338
16757
|
if (key === "actions" || key === "paths") {
|
|
16339
16758
|
continue;
|
|
16340
16759
|
}
|
|
16341
|
-
yield* stringsIn(item,
|
|
16760
|
+
yield* stringsIn(item, path15 ? `${path15}.${key}` : key);
|
|
16342
16761
|
}
|
|
16343
16762
|
}
|
|
16344
16763
|
}
|
|
@@ -16449,7 +16868,7 @@ ${unbacked.map(
|
|
|
16449
16868
|
draft: result.draft,
|
|
16450
16869
|
inlined,
|
|
16451
16870
|
studioUrl,
|
|
16452
|
-
dir:
|
|
16871
|
+
dir: path14.resolve(dir),
|
|
16453
16872
|
unbackedPlaceholders: unbacked
|
|
16454
16873
|
}
|
|
16455
16874
|
);
|
|
@@ -16457,10 +16876,10 @@ ${unbacked.map(
|
|
|
16457
16876
|
}
|
|
16458
16877
|
|
|
16459
16878
|
// src/commands/spec.ts
|
|
16460
|
-
var
|
|
16879
|
+
var import_cli_table36 = __toESM(require_cli_table3(), 1);
|
|
16461
16880
|
var TABLE_STYLE2 = { head: [], border: [], "padding-left": 1, "padding-right": 1 };
|
|
16462
16881
|
function renderList() {
|
|
16463
|
-
const table = new
|
|
16882
|
+
const table = new import_cli_table36.default({
|
|
16464
16883
|
head: ["Type", "Kind", "Label", "Summary"],
|
|
16465
16884
|
colWidths: [40, 9, 24, 70],
|
|
16466
16885
|
wordWrap: true,
|
|
@@ -16488,7 +16907,7 @@ function renderEntry(entry) {
|
|
|
16488
16907
|
const blocks = [`${entry.type} (${entry.kind}) \u2014 ${entry.label}`, entry.summary, `Docs: ${entry.docs}`];
|
|
16489
16908
|
const inputs = Object.entries(entry.inputs);
|
|
16490
16909
|
if (inputs.length) {
|
|
16491
|
-
const table = new
|
|
16910
|
+
const table = new import_cli_table36.default({
|
|
16492
16911
|
head: ["Input", "Type", "Required", "Description"],
|
|
16493
16912
|
colWidths: [20, 9, 10, 90],
|
|
16494
16913
|
wordWrap: true,
|
|
@@ -16738,21 +17157,27 @@ ${item.lines.map((line) => ` ${line}`).join("\n")}`
|
|
|
16738
17157
|
}
|
|
16739
17158
|
|
|
16740
17159
|
// src/commands/workspaces.ts
|
|
16741
|
-
var
|
|
17160
|
+
var import_cli_table37 = __toESM(require_cli_table3(), 1);
|
|
16742
17161
|
function registerWorkspaces(program3) {
|
|
16743
|
-
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) => {
|
|
16744
17163
|
const options = globalOptions(command);
|
|
16745
17164
|
const target = resolveTarget();
|
|
16746
17165
|
const client = await authenticatedClient(target);
|
|
16747
17166
|
const workspaces = await listWorkspaces(client);
|
|
16748
|
-
const table = new
|
|
16749
|
-
head: ["ID", "Name", "Role", "Default"],
|
|
16750
|
-
colWidths: [38,
|
|
17167
|
+
const table = new import_cli_table37.default({
|
|
17168
|
+
head: ["ID", "Name", "Role", "Default", "Scopes"],
|
|
17169
|
+
colWidths: [38, 26, 10, 9, 34],
|
|
16751
17170
|
wordWrap: true,
|
|
16752
17171
|
style: { head: [], border: [], "padding-left": 1, "padding-right": 1 }
|
|
16753
17172
|
});
|
|
16754
17173
|
for (const workspace of workspaces) {
|
|
16755
|
-
table.push([
|
|
17174
|
+
table.push([
|
|
17175
|
+
workspace.id,
|
|
17176
|
+
workspace.name,
|
|
17177
|
+
workspace.userRole ?? "",
|
|
17178
|
+
workspace.isDefault ? "yes" : "",
|
|
17179
|
+
(workspace.scopes ?? []).join(", ")
|
|
17180
|
+
]);
|
|
16756
17181
|
}
|
|
16757
17182
|
emit(
|
|
16758
17183
|
options,
|
|
@@ -16766,11 +17191,12 @@ Pass --workspace <id> to a command, or set SOFTR_WORKSPACE_ID, to work in one of
|
|
|
16766
17191
|
// package.json
|
|
16767
17192
|
var package_default = {
|
|
16768
17193
|
name: "@softr/cli",
|
|
16769
|
-
version: "0.1.
|
|
16770
|
-
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",
|
|
16771
17196
|
keywords: [
|
|
16772
17197
|
"softr",
|
|
16773
17198
|
"workflows",
|
|
17199
|
+
"databases",
|
|
16774
17200
|
"cli"
|
|
16775
17201
|
],
|
|
16776
17202
|
license: "UNLICENSED",
|
|
@@ -16816,7 +17242,7 @@ var package_default = {
|
|
|
16816
17242
|
};
|
|
16817
17243
|
|
|
16818
17244
|
// src/cli.ts
|
|
16819
|
-
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");
|
|
16820
17246
|
registerLogin(program2);
|
|
16821
17247
|
registerWorkspaces(program2);
|
|
16822
17248
|
registerIntegrations(program2);
|
|
@@ -16836,4 +17262,6 @@ registerPublish(workflows);
|
|
|
16836
17262
|
registerPin(workflows);
|
|
16837
17263
|
registerSpec(workflows);
|
|
16838
17264
|
registerSchema(workflows);
|
|
17265
|
+
var db = program2.command("db").description("Manage Softr Databases: databases, their tables, fields and records");
|
|
17266
|
+
registerDatabases(db);
|
|
16839
17267
|
program2.parseAsync(process.argv).catch(reportError);
|