@softr/cli 0.0.1 → 0.1.1-se21216.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +93 -81
- package/dist/cli.mjs +457 -312
- package/package.json +3 -4
package/dist/cli.mjs
CHANGED
|
@@ -1745,7 +1745,7 @@ var require_table = __commonJS({
|
|
|
1745
1745
|
var debug = require_debug();
|
|
1746
1746
|
var utils = require_utils();
|
|
1747
1747
|
var tableLayout = require_layout_manager();
|
|
1748
|
-
var
|
|
1748
|
+
var Table7 = class extends Array {
|
|
1749
1749
|
constructor(opts) {
|
|
1750
1750
|
super();
|
|
1751
1751
|
const options = utils.mergeOptions(opts);
|
|
@@ -1820,7 +1820,7 @@ var require_table = __commonJS({
|
|
|
1820
1820
|
return str[0].length;
|
|
1821
1821
|
}
|
|
1822
1822
|
};
|
|
1823
|
-
|
|
1823
|
+
Table7.reset = () => debug.reset();
|
|
1824
1824
|
function doDraw(row, lineNum, result) {
|
|
1825
1825
|
let line = [];
|
|
1826
1826
|
row.forEach(function(cell) {
|
|
@@ -1829,7 +1829,7 @@ var require_table = __commonJS({
|
|
|
1829
1829
|
let str = line.join("");
|
|
1830
1830
|
if (str.length) result.push(str);
|
|
1831
1831
|
}
|
|
1832
|
-
module.exports =
|
|
1832
|
+
module.exports = Table7;
|
|
1833
1833
|
}
|
|
1834
1834
|
});
|
|
1835
1835
|
|
|
@@ -5278,9 +5278,17 @@ function executionOrder(config) {
|
|
|
5278
5278
|
}
|
|
5279
5279
|
};
|
|
5280
5280
|
for (const trigger of config.triggers ?? []) {
|
|
5281
|
-
add({
|
|
5281
|
+
add({
|
|
5282
|
+
id: trigger.id,
|
|
5283
|
+
type: trigger.type,
|
|
5284
|
+
kind: "trigger",
|
|
5285
|
+
title: trigger.title,
|
|
5286
|
+
testable: true,
|
|
5287
|
+
loopPath: [],
|
|
5288
|
+
reachable: true
|
|
5289
|
+
});
|
|
5282
5290
|
}
|
|
5283
|
-
const collect = (actions,
|
|
5291
|
+
const collect = (actions, loopPath) => {
|
|
5284
5292
|
for (const action of actions ?? []) {
|
|
5285
5293
|
add({
|
|
5286
5294
|
id: action.id,
|
|
@@ -5288,17 +5296,18 @@ function executionOrder(config) {
|
|
|
5288
5296
|
kind: "action",
|
|
5289
5297
|
title: action.title,
|
|
5290
5298
|
testable: !NOT_TESTABLE_TYPES.has(action.type),
|
|
5291
|
-
loopId,
|
|
5299
|
+
loopId: loopPath.at(-1),
|
|
5300
|
+
loopPath,
|
|
5292
5301
|
reachable: false
|
|
5293
5302
|
});
|
|
5294
5303
|
if (action.type === LOOP_TYPE) {
|
|
5295
5304
|
const inputs = action.inputs;
|
|
5296
5305
|
pathSets.push(inputs?.paths ?? []);
|
|
5297
|
-
collect(inputs?.actions, action.id);
|
|
5306
|
+
collect(inputs?.actions, [...loopPath, action.id]);
|
|
5298
5307
|
}
|
|
5299
5308
|
}
|
|
5300
5309
|
};
|
|
5301
|
-
collect(config.actions);
|
|
5310
|
+
collect(config.actions, []);
|
|
5302
5311
|
const outgoing = new Map(appearance.map((id) => [id, []]));
|
|
5303
5312
|
const indegree = new Map(appearance.map((id) => [id, 0]));
|
|
5304
5313
|
for (const path13 of pathSets.flat()) {
|
|
@@ -5342,6 +5351,9 @@ function executionOrder(config) {
|
|
|
5342
5351
|
}
|
|
5343
5352
|
return [...ordered.filter((node) => node.reachable), ...ordered.filter((node) => !node.reachable)];
|
|
5344
5353
|
}
|
|
5354
|
+
function hierarchyPath(node, iteration = 0) {
|
|
5355
|
+
return [...node.loopPath.flatMap((loopId) => [loopId, String(iteration)]), node.id].join("/");
|
|
5356
|
+
}
|
|
5345
5357
|
|
|
5346
5358
|
// src/catalog/catalog.json
|
|
5347
5359
|
var catalog_default = [
|
|
@@ -5698,7 +5710,7 @@ var catalog_default = [
|
|
|
5698
5710
|
integrations: {
|
|
5699
5711
|
type: "array",
|
|
5700
5712
|
required: false,
|
|
5701
|
-
description: 'Workspace integrations the code may call through fetch: [{"alias": "crm", "integrationId": "<uuid>"}]. A call to a host of an attached integration carries its credentials (a REST_API integration only when the call names its alias); the alias ([A-Za-z_][A-Za-z0-9_]*, unique in the node) names one in fetch(url, { integration: "crm" }) when two share a host. Integration ids: `softr
|
|
5713
|
+
description: 'Workspace integrations the code may call through fetch: [{"alias": "crm", "integrationId": "<uuid>"}]. A call to a host of an attached integration carries its credentials (a REST_API integration only when the call names its alias); the alias ([A-Za-z_][A-Za-z0-9_]*, unique in the node) names one in fetch(url, { integration: "crm" }) when two share a host. Integration ids: `softr integrations` lists them, or the studio (Workspace settings \u2192 Integrations, or the URL of the integration).',
|
|
5702
5714
|
example: [
|
|
5703
5715
|
{
|
|
5704
5716
|
alias: "crm",
|
|
@@ -6086,9 +6098,9 @@ var SoftrApiError = class extends Error {
|
|
|
6086
6098
|
body;
|
|
6087
6099
|
hint;
|
|
6088
6100
|
};
|
|
6089
|
-
function hintFor(status) {
|
|
6101
|
+
function hintFor(status, client) {
|
|
6090
6102
|
if (status === 401 || status === 403) {
|
|
6091
|
-
return "The token was not accepted: revoked, from another environment, missing access to this workflow, or its statements do not grant this action. Run: softr
|
|
6103
|
+
return client.kind === "proxy" ? "The target refused the request: no access to this workflow, or its credential is gone." : "The token was not accepted: revoked, from another environment, missing access to this workflow, or its statements do not grant this action. Run: softr login";
|
|
6092
6104
|
}
|
|
6093
6105
|
if (status === 409) {
|
|
6094
6106
|
return "The workflow changed on the server since the last pull. Pull again with --force, then re-apply your edits.";
|
|
@@ -6096,7 +6108,7 @@ function hintFor(status) {
|
|
|
6096
6108
|
return void 0;
|
|
6097
6109
|
}
|
|
6098
6110
|
function authHeaders(client) {
|
|
6099
|
-
return { Authorization: `Bearer ${client.token}` };
|
|
6111
|
+
return client.kind === "proxy" ? {} : { Authorization: `Bearer ${client.token}` };
|
|
6100
6112
|
}
|
|
6101
6113
|
async function send(client, service, method, path13, body) {
|
|
6102
6114
|
const init = {
|
|
@@ -6117,7 +6129,7 @@ async function send(client, service, method, path13, body) {
|
|
|
6117
6129
|
`${method} ${path13} failed with status ${response.status}${text ? `: ${text}` : ""}`,
|
|
6118
6130
|
response.status,
|
|
6119
6131
|
text,
|
|
6120
|
-
hintFor(response.status)
|
|
6132
|
+
hintFor(response.status, client)
|
|
6121
6133
|
);
|
|
6122
6134
|
}
|
|
6123
6135
|
async function apiRequest(client, method, path13, body) {
|
|
@@ -7588,7 +7600,7 @@ async function readConfig(dir) {
|
|
|
7588
7600
|
if (error.code === "ENOENT") {
|
|
7589
7601
|
throw new CliError(
|
|
7590
7602
|
`No ${CONFIG_FILE_NAME} in ${dir}.`,
|
|
7591
|
-
'Run "softr
|
|
7603
|
+
'Run "softr workflows pull <workflowId>" here, or pass --dir.'
|
|
7592
7604
|
);
|
|
7593
7605
|
}
|
|
7594
7606
|
throw error;
|
|
@@ -7619,7 +7631,7 @@ async function findWorkflowDir(cwd, explicitDir) {
|
|
|
7619
7631
|
return cwd;
|
|
7620
7632
|
}
|
|
7621
7633
|
const candidates = await listWorkflowDirs(cwd);
|
|
7622
|
-
const hint = candidates.length ? `Workflows found in: ${candidates.map((dir) => path2.relative(cwd, dir)).join(", ")}. Pass --dir <name> or cd into one.` : 'Run "softr
|
|
7634
|
+
const hint = candidates.length ? `Workflows found in: ${candidates.map((dir) => path2.relative(cwd, dir)).join(", ")}. Pass --dir <name> or cd into one.` : 'Run "softr workflows pull <workflowId>" to create one.';
|
|
7623
7635
|
throw new CliError(`No ${CONFIG_FILE_NAME} in ${cwd}.`, hint);
|
|
7624
7636
|
}
|
|
7625
7637
|
async function listWorkflowDirs(cwd) {
|
|
@@ -7641,6 +7653,79 @@ async function editConfig(dir, edits) {
|
|
|
7641
7653
|
`);
|
|
7642
7654
|
}
|
|
7643
7655
|
|
|
7656
|
+
// src/project/versions.ts
|
|
7657
|
+
function compareVersions(a, b) {
|
|
7658
|
+
const pa = a.split(".").map((part) => Number.parseInt(part, 10) || 0);
|
|
7659
|
+
const pb = b.split(".").map((part) => Number.parseInt(part, 10) || 0);
|
|
7660
|
+
for (let i2 = 0; i2 < Math.max(pa.length, pb.length); i2++) {
|
|
7661
|
+
const diff = (pa[i2] ?? 0) - (pb[i2] ?? 0);
|
|
7662
|
+
if (diff !== 0) {
|
|
7663
|
+
return diff;
|
|
7664
|
+
}
|
|
7665
|
+
}
|
|
7666
|
+
return 0;
|
|
7667
|
+
}
|
|
7668
|
+
var CURRENT = new Map(CATALOG.map((entry) => [entry.type, entry.version]));
|
|
7669
|
+
function outdatedNodes(config) {
|
|
7670
|
+
const outdated = [];
|
|
7671
|
+
for (const node of [...config.triggers ?? [], ...allActions(config.actions)]) {
|
|
7672
|
+
const current2 = CURRENT.get(node.type);
|
|
7673
|
+
if (!current2 || !node.version || compareVersions(node.version, current2) >= 0) {
|
|
7674
|
+
continue;
|
|
7675
|
+
}
|
|
7676
|
+
outdated.push({
|
|
7677
|
+
id: node.id,
|
|
7678
|
+
type: node.type,
|
|
7679
|
+
version: node.version,
|
|
7680
|
+
current: current2,
|
|
7681
|
+
upgradable: upgradeOf(node) !== void 0
|
|
7682
|
+
});
|
|
7683
|
+
}
|
|
7684
|
+
return outdated;
|
|
7685
|
+
}
|
|
7686
|
+
function upgradeOf(node) {
|
|
7687
|
+
const current2 = CURRENT.get(node.type);
|
|
7688
|
+
if (!current2 || !node.version || compareVersions(node.version, current2) >= 0) {
|
|
7689
|
+
return void 0;
|
|
7690
|
+
}
|
|
7691
|
+
if (node.type === CUSTOM_CODE_TYPE) {
|
|
7692
|
+
const inputs = node.inputs ?? {};
|
|
7693
|
+
const ordered = {};
|
|
7694
|
+
for (const key of ["language", "inputData", "integrations", "code"]) {
|
|
7695
|
+
if (key === "integrations") {
|
|
7696
|
+
ordered[key] = inputs[key] ?? [];
|
|
7697
|
+
} else if (inputs[key] !== void 0) {
|
|
7698
|
+
ordered[key] = inputs[key];
|
|
7699
|
+
}
|
|
7700
|
+
}
|
|
7701
|
+
for (const [key, value] of Object.entries(inputs)) {
|
|
7702
|
+
if (!(key in ordered)) {
|
|
7703
|
+
ordered[key] = value;
|
|
7704
|
+
}
|
|
7705
|
+
}
|
|
7706
|
+
return { ...node, version: current2, inputs: ordered };
|
|
7707
|
+
}
|
|
7708
|
+
return void 0;
|
|
7709
|
+
}
|
|
7710
|
+
function versionNote(config) {
|
|
7711
|
+
const outdated = outdatedNodes(config);
|
|
7712
|
+
if (!outdated.length) {
|
|
7713
|
+
return void 0;
|
|
7714
|
+
}
|
|
7715
|
+
const lines = outdated.map(
|
|
7716
|
+
(node) => ` ${node.id}: ${node.type} ${node.version} \u2192 ${node.current}${node.upgradable ? "" : ` (no automatic upgrade; see "spec ${node.type}")`}`
|
|
7717
|
+
);
|
|
7718
|
+
const hint = outdated.some((node) => node.upgradable) ? '\nRun "softr workflows upgrade" to move them to the current version.' : "";
|
|
7719
|
+
return `Nodes on an older version:
|
|
7720
|
+
${lines.join("\n")}${hint}`;
|
|
7721
|
+
}
|
|
7722
|
+
function emitWithVersionNote(options, config, human, json) {
|
|
7723
|
+
const note = versionNote(config);
|
|
7724
|
+
emit(options, note ? `${human}
|
|
7725
|
+
|
|
7726
|
+
${note}` : human, { ...json, versionNote: note ?? null });
|
|
7727
|
+
}
|
|
7728
|
+
|
|
7644
7729
|
// src/commands/edit.ts
|
|
7645
7730
|
import { access as access2, mkdir as mkdir3, writeFile as writeFile4 } from "node:fs/promises";
|
|
7646
7731
|
import path5 from "node:path";
|
|
@@ -13591,6 +13676,36 @@ function renderPyType(type, depth = 0) {
|
|
|
13591
13676
|
}
|
|
13592
13677
|
|
|
13593
13678
|
// src/project/code-wrapper.ts
|
|
13679
|
+
function attachedIntegrations(inputs) {
|
|
13680
|
+
const value = inputs?.["integrations"];
|
|
13681
|
+
if (!Array.isArray(value)) {
|
|
13682
|
+
return [];
|
|
13683
|
+
}
|
|
13684
|
+
return value.flatMap(
|
|
13685
|
+
(entry) => entry && typeof entry === "object" && typeof entry.alias === "string" ? [
|
|
13686
|
+
{
|
|
13687
|
+
alias: entry.alias,
|
|
13688
|
+
integrationId: String(entry.integrationId ?? "")
|
|
13689
|
+
}
|
|
13690
|
+
] : []
|
|
13691
|
+
);
|
|
13692
|
+
}
|
|
13693
|
+
function integrationLines(header) {
|
|
13694
|
+
if (!hasFetch(header.version)) {
|
|
13695
|
+
return [];
|
|
13696
|
+
}
|
|
13697
|
+
const attached = header.integrations ?? [];
|
|
13698
|
+
if (!attached.length) {
|
|
13699
|
+
return [
|
|
13700
|
+
'No integrations attached: add { alias, integrationId } entries under inputs.integrations in softr-workflows.jsonc (ids from "softr integrations").'
|
|
13701
|
+
];
|
|
13702
|
+
}
|
|
13703
|
+
return [
|
|
13704
|
+
"Integrations attached to this node; fetch authenticates a call to their own API hosts by itself:",
|
|
13705
|
+
...attached.map((item) => ` ${item.alias}: ${item.integrationId}`),
|
|
13706
|
+
`Name one with fetch(url, { integration: '${attached[0].alias}' }) only for a REST API integration (it has no host of its own) or when two attached integrations share a host.`
|
|
13707
|
+
];
|
|
13708
|
+
}
|
|
13594
13709
|
var PLACEHOLDER_IN_CODE = /\{\{?[A-Za-z]+[^{}]*:::/;
|
|
13595
13710
|
function parseLanguage(value, nodeId) {
|
|
13596
13711
|
if (value === "JAVASCRIPT" || value === "PYTHON") {
|
|
@@ -13613,17 +13728,6 @@ var FETCH_SINCE = "1.2.0";
|
|
|
13613
13728
|
function hasFetch(version2) {
|
|
13614
13729
|
return compareVersions(version2, FETCH_SINCE) >= 0;
|
|
13615
13730
|
}
|
|
13616
|
-
function compareVersions(a, b) {
|
|
13617
|
-
const pa = a.split(".").map((part) => Number.parseInt(part, 10) || 0);
|
|
13618
|
-
const pb = b.split(".").map((part) => Number.parseInt(part, 10) || 0);
|
|
13619
|
-
for (let i2 = 0; i2 < Math.max(pa.length, pb.length); i2++) {
|
|
13620
|
-
const diff = (pa[i2] ?? 0) - (pb[i2] ?? 0);
|
|
13621
|
-
if (diff !== 0) {
|
|
13622
|
-
return diff;
|
|
13623
|
-
}
|
|
13624
|
-
}
|
|
13625
|
-
return 0;
|
|
13626
|
-
}
|
|
13627
13731
|
var JS_RULES = [
|
|
13628
13732
|
...RULES,
|
|
13629
13733
|
"HTTP calls: `await fetch(url, { method, headers, body, integration })`. An attached integration (inputs.integrations) authenticates calls to its own API hosts; a REST_API integration authenticates only a call that names its alias. No `import`/`require`. An undefined return is reported as null."
|
|
@@ -13650,8 +13754,8 @@ function rules(language, version2) {
|
|
|
13650
13754
|
`This node runs CUSTOM_CODE ${version2}: synchronous, no fetch, inputs.integrations is ignored. Set "version": "1.2.0" on this node in softr-workflows.jsonc to use them.`
|
|
13651
13755
|
];
|
|
13652
13756
|
}
|
|
13653
|
-
var HEADER_START = "softr
|
|
13654
|
-
var HEADER_END = "softr
|
|
13757
|
+
var HEADER_START = "softr: generated header - regenerated on pull, do not edit by hand";
|
|
13758
|
+
var HEADER_END = "softr: end of generated header";
|
|
13655
13759
|
var MAX_EXAMPLE = 60;
|
|
13656
13760
|
function describe(header) {
|
|
13657
13761
|
return `Softr Workflows custom code for node "${header.nodeId}"${header.title ? ` (${header.title})` : ""}.`;
|
|
@@ -13693,6 +13797,7 @@ function wrapCode(language, body, header) {
|
|
|
13693
13797
|
...rules("JAVASCRIPT", header.version).map((rule) => ` * ${rule}`),
|
|
13694
13798
|
" */",
|
|
13695
13799
|
...typedef,
|
|
13800
|
+
...integrationLines(header).map((line) => `// ${line}`),
|
|
13696
13801
|
`// ${HEADER_END}`,
|
|
13697
13802
|
`/** @param {${paramType}} inputData */`,
|
|
13698
13803
|
`export default ${hasFetch(header.version) ? "async " : ""}function (inputData) {`,
|
|
@@ -13712,6 +13817,7 @@ function wrapCode(language, body, header) {
|
|
|
13712
13817
|
...rules("PYTHON", header.version).map((rule) => `# ${rule}`),
|
|
13713
13818
|
"#",
|
|
13714
13819
|
...inputs,
|
|
13820
|
+
...integrationLines(header).map((line) => `# ${line}`),
|
|
13715
13821
|
`# ${HEADER_END}`,
|
|
13716
13822
|
"def main(inputData):",
|
|
13717
13823
|
...lines.length ? lines.map((line) => line === "" ? "" : ` ${line}`) : [" pass"],
|
|
@@ -13948,7 +14054,7 @@ function describePlaceholder(placeholder, raw, config, outputs, depth) {
|
|
|
13948
14054
|
}
|
|
13949
14055
|
}
|
|
13950
14056
|
function untested(nodeId) {
|
|
13951
|
-
return `node "${nodeId}" has no test output yet (run: softr
|
|
14057
|
+
return `node "${nodeId}" has no test output yet (run: softr workflows test ${nodeId})`;
|
|
13952
14058
|
}
|
|
13953
14059
|
function describeOutputRef(nodeId, path13, source, outputs) {
|
|
13954
14060
|
const segments = parsePath(path13);
|
|
@@ -14076,7 +14182,8 @@ async function externalizeCode(config, baseDir, outputs = {}) {
|
|
|
14076
14182
|
nodeId: node.id,
|
|
14077
14183
|
title: node.title,
|
|
14078
14184
|
version: nodeVersion(node),
|
|
14079
|
-
inputs: describeInputData(node, config, outputs)
|
|
14185
|
+
inputs: describeInputData(node, config, outputs),
|
|
14186
|
+
integrations: attachedIntegrations(node.inputs)
|
|
14080
14187
|
})
|
|
14081
14188
|
);
|
|
14082
14189
|
node.inputs["code"] = toFileRef(relative);
|
|
@@ -14131,7 +14238,8 @@ async function refreshCodeHeaders(config, baseDir, outputs) {
|
|
|
14131
14238
|
nodeId: node.id,
|
|
14132
14239
|
title: node.title,
|
|
14133
14240
|
version: nodeVersion(node),
|
|
14134
|
-
inputs: describeInputData(node, config, outputs)
|
|
14241
|
+
inputs: describeInputData(node, config, outputs),
|
|
14242
|
+
integrations: attachedIntegrations(node.inputs)
|
|
14135
14243
|
});
|
|
14136
14244
|
if (next !== source) {
|
|
14137
14245
|
await writeFile3(file, next);
|
|
@@ -14221,8 +14329,9 @@ function registerEdit(program3) {
|
|
|
14221
14329
|
entry.kind === "trigger" && (config.triggers?.length ?? 0) > 1 ? "The service accepts one trigger per published workflow for now." : "",
|
|
14222
14330
|
`Fill inputs.${Object.keys(entry.template)[0] ?? "\u2026"} etc. in softr-workflows.jsonc (see "spec ${entry.type}"), then push and test ${nodeId}.`
|
|
14223
14331
|
].filter(Boolean);
|
|
14224
|
-
|
|
14332
|
+
emitWithVersionNote(
|
|
14225
14333
|
options,
|
|
14334
|
+
await readConfig(dir),
|
|
14226
14335
|
`Replaced "${nodeId}" (${location.node.type}) with ${entry.type}; paths kept.
|
|
14227
14336
|
${notes.join("\n")}`,
|
|
14228
14337
|
{
|
|
@@ -14261,8 +14370,9 @@ ${notes.join("\n")}`,
|
|
|
14261
14370
|
codeFiles.length ? `Code files left in place: ${codeFiles.join(", ")}. Delete them when you no longer need them.` : "",
|
|
14262
14371
|
location.kind === "trigger" && (config.triggers?.length ?? 1) <= 1 ? "The workflow has no trigger now; add one before publishing." : ""
|
|
14263
14372
|
].filter(Boolean);
|
|
14264
|
-
|
|
14373
|
+
emitWithVersionNote(
|
|
14265
14374
|
options,
|
|
14375
|
+
await readConfig(dir),
|
|
14266
14376
|
`Removed ${removedNodes.map((id) => `"${id}"`).join(", ")} and ${doomed.length} path(s).${notes.length ? `
|
|
14267
14377
|
${notes.join("\n")}` : ""}`,
|
|
14268
14378
|
{
|
|
@@ -14287,13 +14397,12 @@ function updateWorkflow(client, workflowId, body) {
|
|
|
14287
14397
|
function getWorkflowOutputs(client, workflowId) {
|
|
14288
14398
|
return apiRequest(client, "GET", `/v1/workflows/${workflowId}/outputs`);
|
|
14289
14399
|
}
|
|
14290
|
-
function listWorkflows(client, workspaceId) {
|
|
14291
|
-
const query = new URLSearchParams({ workspaceId, limit: "500" });
|
|
14400
|
+
function listWorkflows(client, workspaceId, applicationId) {
|
|
14401
|
+
const query = new URLSearchParams({ workspaceId, ...applicationId ? { applicationId } : {}, limit: "500" });
|
|
14292
14402
|
return apiRequest(client, "GET", `/v1/workflows?${query}`);
|
|
14293
14403
|
}
|
|
14294
|
-
function testAction(client, workflowId,
|
|
14295
|
-
|
|
14296
|
-
return apiRequest(client, "POST", `/v1/workflows/${workflowId}/outputs/actions/${actionId}/${mode}${query}`, {});
|
|
14404
|
+
function testAction(client, workflowId, hierarchyPath2, mode) {
|
|
14405
|
+
return apiRequest(client, "POST", `/v1/workflows/${workflowId}/outputs/${mode}/actions/${hierarchyPath2}`, {});
|
|
14297
14406
|
}
|
|
14298
14407
|
function testTrigger(client, workflowId, triggerId) {
|
|
14299
14408
|
return apiRequest(client, "POST", `/v1/workflows/${workflowId}/outputs/triggers/${triggerId}/test`, {});
|
|
@@ -14320,18 +14429,8 @@ async function getEnabledVersion(client, workflowId) {
|
|
|
14320
14429
|
throw error;
|
|
14321
14430
|
}
|
|
14322
14431
|
}
|
|
14323
|
-
|
|
14324
|
-
|
|
14325
|
-
return await apiRequest(client, "GET", `/v1/workflows/${workflowId}/readiness`);
|
|
14326
|
-
} catch (error) {
|
|
14327
|
-
if (error instanceof SoftrApiError && (error.status === 404 || isUnknownRouteAnswer(error))) {
|
|
14328
|
-
return null;
|
|
14329
|
-
}
|
|
14330
|
-
throw error;
|
|
14331
|
-
}
|
|
14332
|
-
}
|
|
14333
|
-
function isUnknownRouteAnswer(error) {
|
|
14334
|
-
return error.status === 500 && error.body.includes("ApiErrorMessages.INTERNAL_SERVER_ERROR");
|
|
14432
|
+
function getWorkflowReadiness(client, workflowId) {
|
|
14433
|
+
return apiRequest(client, "GET", `/v1/workflows/${workflowId}/readiness`);
|
|
14335
14434
|
}
|
|
14336
14435
|
|
|
14337
14436
|
// src/config/credentials.ts
|
|
@@ -14341,7 +14440,7 @@ import path6 from "node:path";
|
|
|
14341
14440
|
|
|
14342
14441
|
// src/api/oauth.ts
|
|
14343
14442
|
import { createHash as createHash2, randomBytes } from "node:crypto";
|
|
14344
|
-
var OAUTH_CLIENT_ID_ENV_VAR = "
|
|
14443
|
+
var OAUTH_CLIENT_ID_ENV_VAR = "SOFTR_OAUTH_CLIENT_ID";
|
|
14345
14444
|
var DEFAULT_OAUTH_CLIENT_ID = "74341326-0061-4f0e-965c-a5aa14a9a89a";
|
|
14346
14445
|
function oauthClientId() {
|
|
14347
14446
|
return process.env[OAUTH_CLIENT_ID_ENV_VAR] || DEFAULT_OAUTH_CLIENT_ID;
|
|
@@ -14379,7 +14478,7 @@ async function tokenRequest(target, form) {
|
|
|
14379
14478
|
`POST ${path13} failed with status ${response.status}${text ? `: ${text}` : ""}`,
|
|
14380
14479
|
response.status,
|
|
14381
14480
|
text,
|
|
14382
|
-
form["grant_type"] === "refresh_token" ? "The OAuth grant is gone (revoked or older than 30 days). Run: softr
|
|
14481
|
+
form["grant_type"] === "refresh_token" ? "The OAuth grant is gone (revoked or older than 30 days). Run: softr login" : void 0
|
|
14383
14482
|
);
|
|
14384
14483
|
}
|
|
14385
14484
|
const tokens = await response.json();
|
|
@@ -14403,8 +14502,8 @@ function refreshTokens(target, refreshToken) {
|
|
|
14403
14502
|
|
|
14404
14503
|
// src/config/target.ts
|
|
14405
14504
|
var API_URL_ENV_VAR = "SOFTR_WORKFLOWS_API_URL";
|
|
14406
|
-
var STUDIO_API_URL_ENV_VAR = "
|
|
14407
|
-
var STUDIO_URL_ENV_VAR = "
|
|
14505
|
+
var STUDIO_API_URL_ENV_VAR = "SOFTR_STUDIO_API_URL";
|
|
14506
|
+
var STUDIO_URL_ENV_VAR = "SOFTR_STUDIO_URL";
|
|
14408
14507
|
var DEFAULT_API_URL = "https://workflows-api.softr.io";
|
|
14409
14508
|
var DEFAULT_STUDIO_API_URL = "https://studio-api.softr.io";
|
|
14410
14509
|
var DEFAULT_STUDIO_URL = "https://studio.softr.io";
|
|
@@ -14415,7 +14514,7 @@ function normalizeUrl(value, source) {
|
|
|
14415
14514
|
} catch {
|
|
14416
14515
|
throw new CliError(`${source} is not a valid URL: ${value}`);
|
|
14417
14516
|
}
|
|
14418
|
-
return url.origin
|
|
14517
|
+
return `${url.origin}${url.pathname.replace(/\/+$/, "")}`;
|
|
14419
14518
|
}
|
|
14420
14519
|
function resolveTarget(remembered) {
|
|
14421
14520
|
const pick = (envVar, rememberedValue, fallback) => {
|
|
@@ -14429,7 +14528,7 @@ function resolveTarget(remembered) {
|
|
|
14429
14528
|
};
|
|
14430
14529
|
}
|
|
14431
14530
|
function targetHost(target) {
|
|
14432
|
-
return new URL(target.
|
|
14531
|
+
return new URL(target.studioApiUrl).host;
|
|
14433
14532
|
}
|
|
14434
14533
|
|
|
14435
14534
|
// src/config/tokens.ts
|
|
@@ -14442,12 +14541,14 @@ function tokenKind(token) {
|
|
|
14442
14541
|
}
|
|
14443
14542
|
throw new CliError(
|
|
14444
14543
|
"The token is neither a personal API token (pat_...) nor an OAuth access token (oaa_...).",
|
|
14445
|
-
'Create a personal API token in the studio and paste the whole value, or run "softr
|
|
14544
|
+
'Create a personal API token in the studio and paste the whole value, or run "softr login" without --token to log in through the browser.'
|
|
14446
14545
|
);
|
|
14447
14546
|
}
|
|
14448
14547
|
|
|
14449
14548
|
// src/config/credentials.ts
|
|
14450
|
-
var TOKEN_ENV_VAR = "
|
|
14549
|
+
var TOKEN_ENV_VAR = "SOFTR_TOKEN";
|
|
14550
|
+
var AUTH_ENV_VAR = "SOFTR_AUTH";
|
|
14551
|
+
var PROXY_AUTH = "proxy";
|
|
14451
14552
|
var REFRESH_MARGIN_MS = 5 * 60 * 1e3;
|
|
14452
14553
|
function credentialsPath() {
|
|
14453
14554
|
return path6.join(homedir(), ".softr", "credentials.json");
|
|
@@ -14512,13 +14613,16 @@ async function refreshedAccessToken(target, stored) {
|
|
|
14512
14613
|
}
|
|
14513
14614
|
}
|
|
14514
14615
|
async function authenticatedClient(target) {
|
|
14616
|
+
if (process.env[AUTH_ENV_VAR] === PROXY_AUTH) {
|
|
14617
|
+
return { target, kind: "proxy" };
|
|
14618
|
+
}
|
|
14515
14619
|
const fromEnv = process.env[TOKEN_ENV_VAR];
|
|
14516
14620
|
if (fromEnv) {
|
|
14517
14621
|
return { target, token: fromEnv, kind: tokenKind(fromEnv) };
|
|
14518
14622
|
}
|
|
14519
14623
|
const stored = await storedToken(target);
|
|
14520
14624
|
if (!stored) {
|
|
14521
|
-
throw new CliError(`No token stored for ${targetHost(target)}.`, "Run: softr
|
|
14625
|
+
throw new CliError(`No token stored for ${targetHost(target)}.`, "Run: softr login");
|
|
14522
14626
|
}
|
|
14523
14627
|
const kind = stored.kind ?? tokenKind(stored.token);
|
|
14524
14628
|
const expiresAt = stored.expiresAt ? new Date(stored.expiresAt) : void 0;
|
|
@@ -14528,7 +14632,7 @@ async function authenticatedClient(target) {
|
|
|
14528
14632
|
if (expiresAt && expiresAt < /* @__PURE__ */ new Date()) {
|
|
14529
14633
|
throw new CliError(
|
|
14530
14634
|
`The stored token for ${targetHost(target)} expired at ${stored.expiresAt}.`,
|
|
14531
|
-
"Run: softr
|
|
14635
|
+
"Run: softr login"
|
|
14532
14636
|
);
|
|
14533
14637
|
}
|
|
14534
14638
|
return { target, token: stored.token, kind };
|
|
@@ -14545,7 +14649,7 @@ async function resolveTokenOwner(client) {
|
|
|
14545
14649
|
if (error instanceof SoftrApiError && error.status === 404) {
|
|
14546
14650
|
throw new CliError(
|
|
14547
14651
|
"The studio does not know this token: revoked, expired, or from another environment.",
|
|
14548
|
-
'Create a new personal API token in the studio, or run "softr
|
|
14652
|
+
'Create a new personal API token in the studio, or run "softr login" for the browser login.'
|
|
14549
14653
|
);
|
|
14550
14654
|
}
|
|
14551
14655
|
throw error;
|
|
@@ -14621,7 +14725,7 @@ function toRow(workflow, workspace) {
|
|
|
14621
14725
|
updatedAt: workflow.updatedAt ?? null
|
|
14622
14726
|
};
|
|
14623
14727
|
}
|
|
14624
|
-
async function collectWorkflows(client, workspaceId) {
|
|
14728
|
+
async function collectWorkflows(client, workspaceId, applicationId) {
|
|
14625
14729
|
const workspaces = await listWorkspaces(client);
|
|
14626
14730
|
const selected = workspaceId ? workspaces.filter((workspace) => workspace.id === workspaceId) : workspaces;
|
|
14627
14731
|
if (workspaceId && !selected.length) {
|
|
@@ -14633,7 +14737,7 @@ async function collectWorkflows(client, workspaceId) {
|
|
|
14633
14737
|
const listing = { workspaces: [], workflows: [], failures: [] };
|
|
14634
14738
|
for (const workspace of selected) {
|
|
14635
14739
|
try {
|
|
14636
|
-
const page = await listWorkflows(client, workspace.id);
|
|
14740
|
+
const page = await listWorkflows(client, workspace.id, applicationId);
|
|
14637
14741
|
listing.workspaces.push({ id: workspace.id, name: workspace.name });
|
|
14638
14742
|
listing.workflows.push(...page.items.map((workflow) => toRow(workflow, workspace)));
|
|
14639
14743
|
} catch (error) {
|
|
@@ -14663,10 +14767,10 @@ function workflowTable(rows) {
|
|
|
14663
14767
|
}
|
|
14664
14768
|
return table.toString();
|
|
14665
14769
|
}
|
|
14666
|
-
function renderWorkflows({ workspaces, workflows, failures }) {
|
|
14770
|
+
function renderWorkflows({ workspaces, workflows: workflows2, failures }) {
|
|
14667
14771
|
const blocks = [];
|
|
14668
14772
|
for (const workspace of workspaces) {
|
|
14669
|
-
const rows =
|
|
14773
|
+
const rows = workflows2.filter((row) => row.workspaceId === workspace.id);
|
|
14670
14774
|
const heading = `Workspace: ${workspace.name} (${workspace.id})`;
|
|
14671
14775
|
blocks.push(rows.length ? `${heading}
|
|
14672
14776
|
${workflowTable(rows)}` : `${heading}
|
|
@@ -14684,16 +14788,16 @@ ${workflowTable(rows)}` : `${heading}
|
|
|
14684
14788
|
}
|
|
14685
14789
|
return blocks.join("\n\n");
|
|
14686
14790
|
}
|
|
14687
|
-
async function emitWorkflowList(options, client, workspaceId, human) {
|
|
14688
|
-
const listing = await collectWorkflows(client, workspaceId);
|
|
14791
|
+
async function emitWorkflowList(options, client, workspaceId, human, applicationId) {
|
|
14792
|
+
const listing = await collectWorkflows(client, workspaceId, applicationId);
|
|
14689
14793
|
emit(options, [human, renderWorkflows(listing)].filter(Boolean).join("\n\n"), { ...listing });
|
|
14690
14794
|
}
|
|
14691
14795
|
function registerList(program3) {
|
|
14692
|
-
program3.command("list").description("List the workflows you can access, grouped by workspace").option("--workspace <id>", "only this workspace").action(async (opts, command) => {
|
|
14796
|
+
program3.command("list").description("List the workflows you can access, grouped by workspace").option("--workspace <id>", "only this workspace (default: $SOFTR_WORKSPACE_ID when set)").option("--app <id>", "only the workflows linked to this application").action(async (opts, command) => {
|
|
14693
14797
|
const options = globalOptions(command);
|
|
14694
14798
|
const target = resolveTarget(await readState(process.cwd()));
|
|
14695
14799
|
const client = await authenticatedClient(target);
|
|
14696
|
-
await emitWorkflowList(options, client, opts.workspace);
|
|
14800
|
+
await emitWorkflowList(options, client, opts.workspace ?? process.env["SOFTR_WORKSPACE_ID"], void 0, opts.app);
|
|
14697
14801
|
});
|
|
14698
14802
|
}
|
|
14699
14803
|
|
|
@@ -14960,7 +15064,7 @@ function registerPull(program3) {
|
|
|
14960
15064
|
options,
|
|
14961
15065
|
client,
|
|
14962
15066
|
void 0,
|
|
14963
|
-
"Pick a workflow and run: softr
|
|
15067
|
+
"Pick a workflow and run: softr workflows pull <workflowId>"
|
|
14964
15068
|
);
|
|
14965
15069
|
return;
|
|
14966
15070
|
}
|
|
@@ -14991,8 +15095,9 @@ Sample outputs not available, inputData types are unknown: ${error.message}`;
|
|
|
14991
15095
|
outputsFile = await writeStateFile(dir, "outputs.json", rows);
|
|
14992
15096
|
}
|
|
14993
15097
|
const files = [configFile, ...codeFiles].map((file) => path8.relative(cwd, file));
|
|
14994
|
-
|
|
15098
|
+
emitWithVersionNote(
|
|
14995
15099
|
options,
|
|
15100
|
+
config,
|
|
14996
15101
|
`Pulled "${workflow.title}" (version ${workflow.version}, draft ${workflow.draft}, ${workflow.enabled ? "enabled" : "not enabled"}).
|
|
14997
15102
|
` + files.map((file) => ` ${file}`).join("\n") + outputsNote,
|
|
14998
15103
|
{
|
|
@@ -15079,10 +15184,11 @@ function registerAdd(program3) {
|
|
|
15079
15184
|
const notes = [
|
|
15080
15185
|
(config.triggers?.length ?? 0) >= 1 ? "The service accepts one trigger per published workflow for now; multi-trigger support is coming." : "",
|
|
15081
15186
|
opts.to ? "" : `Connect it with a path to the first action (or pass --to next time).`,
|
|
15082
|
-
`Then: softr
|
|
15187
|
+
`Then: softr workflows push, softr workflows test ${id}.`
|
|
15083
15188
|
].filter(Boolean);
|
|
15084
|
-
|
|
15189
|
+
emitWithVersionNote(
|
|
15085
15190
|
options,
|
|
15191
|
+
config,
|
|
15086
15192
|
`Added ${entry.type} trigger "${id}"${opts.to ? ` leading to "${opts.to}"` : ""}.
|
|
15087
15193
|
${notes.join("\n")}`,
|
|
15088
15194
|
{
|
|
@@ -15113,10 +15219,11 @@ ${notes.join("\n")}`,
|
|
|
15113
15219
|
const next = [
|
|
15114
15220
|
codeFile ? `Write the function in ${path9.relative(process.cwd(), codeFile)} and map its inputs under inputs.inputData in softr-workflows.jsonc.` : "",
|
|
15115
15221
|
entry.type === "BRANCH" || entry.type === "FILTER" ? `Add the condition(s) on the path(s) leaving it in softr-workflows.jsonc (see "spec ${entry.type}").` : "",
|
|
15116
|
-
`Then: softr
|
|
15222
|
+
`Then: softr workflows push, softr workflows test ${id}.`
|
|
15117
15223
|
].filter(Boolean);
|
|
15118
|
-
|
|
15224
|
+
emitWithVersionNote(
|
|
15119
15225
|
options,
|
|
15226
|
+
config,
|
|
15120
15227
|
`Added ${entry.type} "${id}" after "${after}"${opts.inLoop ? ` inside loop "${opts.inLoop}"` : ""}.
|
|
15121
15228
|
${next.join("\n")}`,
|
|
15122
15229
|
{
|
|
@@ -15158,58 +15265,66 @@ async function resolveWorkspace(client, requested) {
|
|
|
15158
15265
|
);
|
|
15159
15266
|
}
|
|
15160
15267
|
function registerInit(program3) {
|
|
15161
|
-
program3.command("init").description("Create a new workflow with one trigger, on the server and as a local directory").argument("<title>", "the workflow title").option(
|
|
15162
|
-
|
|
15163
|
-
|
|
15164
|
-
|
|
15165
|
-
|
|
15166
|
-
|
|
15167
|
-
|
|
15168
|
-
|
|
15169
|
-
|
|
15170
|
-
|
|
15171
|
-
|
|
15172
|
-
|
|
15173
|
-
|
|
15174
|
-
|
|
15175
|
-
|
|
15176
|
-
|
|
15177
|
-
|
|
15178
|
-
actions: [],
|
|
15179
|
-
paths: [],
|
|
15180
|
-
// Like the studio: the author's time zone, so schedules and waits mean what they read.
|
|
15181
|
-
configuration: { timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC" }
|
|
15182
|
-
});
|
|
15183
|
-
const workflow = await getWorkflow(client, created.id);
|
|
15184
|
-
const config = { $schema: SCHEMA_POINTER, ...toLocalConfig(workflow) };
|
|
15185
|
-
const configFile = await writeConfig(dir, config);
|
|
15186
|
-
await writeSchemaFile(dir);
|
|
15187
|
-
const syncedHash = definitionHash((await packWorkflow(config, dir)).packed);
|
|
15188
|
-
await writeState(dir, {
|
|
15189
|
-
...target,
|
|
15190
|
-
workflowId: created.id,
|
|
15191
|
-
version: workflow.version,
|
|
15192
|
-
draft: workflow.draft,
|
|
15193
|
-
syncedHash
|
|
15194
|
-
});
|
|
15195
|
-
const studioUrl = `${target.studioUrl}/workflow/${created.id}`;
|
|
15196
|
-
emit(
|
|
15197
|
-
options,
|
|
15198
|
-
`Created "${title}" (${created.id}) with a ${trigger.type} trigger "${TRIGGER_ID}" in workspace ${workspaceId}.
|
|
15199
|
-
${path10.relative(cwd, configFile)}
|
|
15200
|
-
${studioUrl}
|
|
15201
|
-
Next: softr-workflows add <TYPE> --dir ${path10.relative(cwd, dir) || "."} (see "spec" for the types), then push and test.`,
|
|
15202
|
-
{
|
|
15203
|
-
workflowId: created.id,
|
|
15268
|
+
program3.command("init").description("Create a new workflow with one trigger, on the server and as a local directory").argument("<title>", "the workflow title").option(
|
|
15269
|
+
"--workspace <id>",
|
|
15270
|
+
"workspace to create it in (default: $SOFTR_WORKSPACE_ID, else your only or default workspace)"
|
|
15271
|
+
).option("--app <id>", "application to link the workflow to (default: $SOFTR_APP_ID when set)").option("--trigger <TYPE>", 'trigger type from "spec" (default WEBHOOK)', "WEBHOOK").option("--dir <path>", "directory to create (default: ./<title-slug>)").action(
|
|
15272
|
+
async (title, opts, command) => {
|
|
15273
|
+
const options = globalOptions(command);
|
|
15274
|
+
const cwd = process.cwd();
|
|
15275
|
+
const dir = path10.resolve(cwd, opts.dir ?? slug(title));
|
|
15276
|
+
if (await configExists(dir)) {
|
|
15277
|
+
throw new CliError(`${path10.join(dir, CONFIG_FILE_NAME)} already exists.`, "Pick another --dir or title.");
|
|
15278
|
+
}
|
|
15279
|
+
const trigger = findCatalogEntry(opts.trigger, "trigger");
|
|
15280
|
+
const target = resolveTarget();
|
|
15281
|
+
const client = await authenticatedClient(target);
|
|
15282
|
+
const workspaceId = await resolveWorkspace(client, opts.workspace ?? process.env["SOFTR_WORKSPACE_ID"]);
|
|
15283
|
+
const pinToApplicationId = opts.app ?? process.env["SOFTR_APP_ID"];
|
|
15284
|
+
const created = await createWorkflow(client, {
|
|
15204
15285
|
workspaceId,
|
|
15286
|
+
applicationId: null,
|
|
15287
|
+
...pinToApplicationId ? { pinToApplicationId } : {},
|
|
15205
15288
|
title,
|
|
15206
|
-
|
|
15207
|
-
|
|
15208
|
-
|
|
15209
|
-
|
|
15210
|
-
|
|
15211
|
-
|
|
15212
|
-
|
|
15289
|
+
thumbnail: null,
|
|
15290
|
+
triggers: [{ id: TRIGGER_ID, type: trigger.type, version: trigger.version, inputs: trigger.template }],
|
|
15291
|
+
actions: [],
|
|
15292
|
+
paths: [],
|
|
15293
|
+
// Like the studio: the author's time zone, so schedules and waits mean what they read.
|
|
15294
|
+
configuration: { timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC" }
|
|
15295
|
+
});
|
|
15296
|
+
const workflow = await getWorkflow(client, created.id);
|
|
15297
|
+
const config = { $schema: SCHEMA_POINTER, ...toLocalConfig(workflow) };
|
|
15298
|
+
const configFile = await writeConfig(dir, config);
|
|
15299
|
+
await writeSchemaFile(dir);
|
|
15300
|
+
const syncedHash = definitionHash((await packWorkflow(config, dir)).packed);
|
|
15301
|
+
await writeState(dir, {
|
|
15302
|
+
...target,
|
|
15303
|
+
workflowId: created.id,
|
|
15304
|
+
version: workflow.version,
|
|
15305
|
+
draft: workflow.draft,
|
|
15306
|
+
syncedHash
|
|
15307
|
+
});
|
|
15308
|
+
const studioUrl = `${target.studioUrl}/workflow/${created.id}`;
|
|
15309
|
+
emit(
|
|
15310
|
+
options,
|
|
15311
|
+
`Created "${title}" (${created.id}) with a ${trigger.type} trigger "${TRIGGER_ID}" in workspace ${workspaceId}${pinToApplicationId ? `, linked to application ${pinToApplicationId}` : ""}.
|
|
15312
|
+
${path10.relative(cwd, configFile)}
|
|
15313
|
+
${studioUrl}
|
|
15314
|
+
Next: softr workflows add <TYPE> --dir ${path10.relative(cwd, dir) || "."} (see "spec" for the types), then push and test.`,
|
|
15315
|
+
{
|
|
15316
|
+
workflowId: created.id,
|
|
15317
|
+
workspaceId,
|
|
15318
|
+
pinnedToApplicationId: pinToApplicationId ?? null,
|
|
15319
|
+
title,
|
|
15320
|
+
triggerId: TRIGGER_ID,
|
|
15321
|
+
triggerType: trigger.type,
|
|
15322
|
+
dir,
|
|
15323
|
+
studioUrl
|
|
15324
|
+
}
|
|
15325
|
+
);
|
|
15326
|
+
}
|
|
15327
|
+
);
|
|
15213
15328
|
}
|
|
15214
15329
|
|
|
15215
15330
|
// src/commands/integrations.ts
|
|
@@ -15263,12 +15378,15 @@ async function workspaceFor(client, requested, workflowDir) {
|
|
|
15263
15378
|
return resolveWorkspace(client, void 0);
|
|
15264
15379
|
}
|
|
15265
15380
|
function registerIntegrations(program3) {
|
|
15266
|
-
program3.command("integrations").description("List the workspace's connected integrations, whose ids go into integrationId inputs").option(
|
|
15381
|
+
program3.command("integrations").description("List the workspace's connected integrations, whose ids go into integrationId inputs").option(
|
|
15382
|
+
"--workspace <id>",
|
|
15383
|
+
"workspace to list (default: $SOFTR_WORKSPACE_ID, else the workflow directory's, else your only or default workspace)"
|
|
15384
|
+
).option("--type <TYPE>", "only this integration type, e.g. SLACK or SOFTR_TABLES").option("--dir <path>", "workflow directory whose workspace to use (default: the current directory)").action(async (opts, command) => {
|
|
15267
15385
|
const options = globalOptions(command);
|
|
15268
15386
|
const workflowDir = await workflowDirOf(process.cwd(), opts.dir);
|
|
15269
15387
|
const target = resolveTarget(await readState(workflowDir ?? process.cwd()));
|
|
15270
15388
|
const client = await authenticatedClient(target);
|
|
15271
|
-
const workspaceId = await workspaceFor(client, opts.workspace, workflowDir);
|
|
15389
|
+
const workspaceId = await workspaceFor(client, opts.workspace ?? process.env["SOFTR_WORKSPACE_ID"], workflowDir);
|
|
15272
15390
|
const wanted = opts.type?.toUpperCase();
|
|
15273
15391
|
const rows = (await listWorkspaceIntegrations(client, workspaceId)).map(toRow2).filter((row) => !wanted || row.type === wanted).sort(byTypeThenName);
|
|
15274
15392
|
emit(options, `Workspace ${workspaceId}${wanted ? `, type ${wanted}` : ""}:
|
|
@@ -15322,7 +15440,7 @@ async function interactiveLogin(target) {
|
|
|
15322
15440
|
if (answer === "2") {
|
|
15323
15441
|
const token = (await rl.question("Personal API token: ")).trim();
|
|
15324
15442
|
if (!token) {
|
|
15325
|
-
throw new CliError("No token given.", 'Run "softr
|
|
15443
|
+
throw new CliError("No token given.", 'Run "softr login" again.');
|
|
15326
15444
|
}
|
|
15327
15445
|
return pastedToken(token);
|
|
15328
15446
|
}
|
|
@@ -15350,7 +15468,7 @@ function waitForCallback(expectedState) {
|
|
|
15350
15468
|
}
|
|
15351
15469
|
const finish = (status, text) => {
|
|
15352
15470
|
response.writeHead(status, { "Content-Type": "text/html; charset=utf-8", Connection: "close" });
|
|
15353
|
-
response.end(`<!doctype html><title>softr
|
|
15471
|
+
response.end(`<!doctype html><title>softr</title><p>${text} You can close this tab.</p>`);
|
|
15354
15472
|
server.close();
|
|
15355
15473
|
};
|
|
15356
15474
|
const error = url.searchParams.get("error");
|
|
@@ -15377,9 +15495,7 @@ function waitForCallback(expectedState) {
|
|
|
15377
15495
|
}
|
|
15378
15496
|
const timer = setTimeout(() => {
|
|
15379
15497
|
server.close();
|
|
15380
|
-
settle?.reject(
|
|
15381
|
-
new CliError("No answer from the browser within 5 minutes.", 'Run "softr-workflows login" again.')
|
|
15382
|
-
);
|
|
15498
|
+
settle?.reject(new CliError("No answer from the browser within 5 minutes.", 'Run "softr login" again.'));
|
|
15383
15499
|
}, CALLBACK_TIMEOUT_MS);
|
|
15384
15500
|
timer.unref();
|
|
15385
15501
|
resolveServer({ redirectUri: `http://127.0.0.1:${address.port}/callback`, result });
|
|
@@ -15436,6 +15552,11 @@ function registerLogin(program3) {
|
|
|
15436
15552
|
).option("--no-browser", "browser login, but print the URL instead of opening it (for remote shells and agents)").action(async (opts, command) => {
|
|
15437
15553
|
const options = globalOptions(command);
|
|
15438
15554
|
const target = resolveTarget();
|
|
15555
|
+
if (process.env[AUTH_ENV_VAR] === "proxy") {
|
|
15556
|
+
throw new CliError(
|
|
15557
|
+
`Login is not needed: the target authenticates the requests itself ($${AUTH_ENV_VAR}=proxy).`
|
|
15558
|
+
);
|
|
15559
|
+
}
|
|
15439
15560
|
const token = (opts.token ?? process.env[TOKEN_ENV_VAR] ?? "").trim();
|
|
15440
15561
|
const saved = token ? pastedToken(token) : opts.browser ? await interactiveLogin(target) : await browserLogin(target, true);
|
|
15441
15562
|
const identity = await identify({ target, token: saved.token, kind: saved.kind });
|
|
@@ -15455,6 +15576,14 @@ function registerLogin(program3) {
|
|
|
15455
15576
|
const options = globalOptions(command);
|
|
15456
15577
|
const target = resolveTarget();
|
|
15457
15578
|
const client = await authenticatedClient(target);
|
|
15579
|
+
if (client.kind === "proxy") {
|
|
15580
|
+
emit(
|
|
15581
|
+
options,
|
|
15582
|
+
`Requests to ${targetHost(target)} carry no token: the target authenticates them itself ($${AUTH_ENV_VAR}=proxy).`,
|
|
15583
|
+
{ kind: "proxy", host: targetHost(target) }
|
|
15584
|
+
);
|
|
15585
|
+
return;
|
|
15586
|
+
}
|
|
15458
15587
|
const identity = await identify(client);
|
|
15459
15588
|
const stored = process.env[TOKEN_ENV_VAR] ? void 0 : await storedToken(target);
|
|
15460
15589
|
const expiry = stored?.expiresAt ? ` Expires ${stored.expiresAt.slice(0, 16).replace("T", " ")} UTC.` : "";
|
|
@@ -15484,51 +15613,6 @@ function outdatedReferences(row) {
|
|
|
15484
15613
|
const outdated = row?.sampleOutput?.outdatedDependencies;
|
|
15485
15614
|
return outdated && Object.keys(outdated).length ? outdated : {};
|
|
15486
15615
|
}
|
|
15487
|
-
function assessTests(config, rows, lastSavedAt) {
|
|
15488
|
-
const byId = new Map(rows.map((row) => [row.id, row]));
|
|
15489
|
-
const issues = [];
|
|
15490
|
-
for (const node of executionOrder(config)) {
|
|
15491
|
-
if (!node.testable) {
|
|
15492
|
-
continue;
|
|
15493
|
-
}
|
|
15494
|
-
const row = byId.get(node.id);
|
|
15495
|
-
const result = row?.sampleOutput?.result;
|
|
15496
|
-
if (!result) {
|
|
15497
|
-
issues.push({ id: node.id, type: node.type, kind: "untested", detail: "not tested" });
|
|
15498
|
-
} else if (result.status === "FAILED") {
|
|
15499
|
-
issues.push({
|
|
15500
|
-
id: node.id,
|
|
15501
|
-
type: node.type,
|
|
15502
|
-
kind: "failed",
|
|
15503
|
-
detail: `last test failed${result.errorMessage ? `: ${result.errorMessage}` : ""}`
|
|
15504
|
-
});
|
|
15505
|
-
} else if (result.status !== "SUCCEEDED") {
|
|
15506
|
-
issues.push({
|
|
15507
|
-
id: node.id,
|
|
15508
|
-
type: node.type,
|
|
15509
|
-
kind: "pending",
|
|
15510
|
-
detail: `last test ended with ${result.status ?? "no status"}`
|
|
15511
|
-
});
|
|
15512
|
-
} else if (lastSavedAt && row?.sampleOutput?.createdAt && new Date(row.sampleOutput.createdAt) < new Date(lastSavedAt)) {
|
|
15513
|
-
issues.push({
|
|
15514
|
-
id: node.id,
|
|
15515
|
-
type: node.type,
|
|
15516
|
-
kind: "stale",
|
|
15517
|
-
detail: `tested ${row.sampleOutput.createdAt}, before the last save ${lastSavedAt}; test again to be sure`
|
|
15518
|
-
});
|
|
15519
|
-
}
|
|
15520
|
-
const outdated = Object.entries(outdatedReferences(row));
|
|
15521
|
-
if (outdated.length) {
|
|
15522
|
-
issues.push({
|
|
15523
|
-
id: node.id,
|
|
15524
|
-
type: node.type,
|
|
15525
|
-
kind: "outdated",
|
|
15526
|
-
detail: `tested against outdated data of ${outdated.map(([upstream, paths]) => `${upstream} (${paths.join(", ")})`).join("; ")}`
|
|
15527
|
-
});
|
|
15528
|
-
}
|
|
15529
|
-
}
|
|
15530
|
-
return issues;
|
|
15531
|
-
}
|
|
15532
15616
|
function renderIssues(issues) {
|
|
15533
15617
|
return issues.map((issue) => ` - ${issue.id} (${issue.type}): ${issue.detail}`).join("\n");
|
|
15534
15618
|
}
|
|
@@ -15618,7 +15702,7 @@ async function runOne(client, workflowId, node, opts) {
|
|
|
15618
15702
|
if (!node.testable) {
|
|
15619
15703
|
return { ...base, outcome: "skipped", detail: "branches, filters and loops have no output of their own" };
|
|
15620
15704
|
}
|
|
15621
|
-
const row = node.kind === "trigger" ? await runTrigger(client, workflowId, node.id, opts.sample ?? 0) : await testAction(client, workflowId, node.
|
|
15705
|
+
const row = node.kind === "trigger" ? await runTrigger(client, workflowId, node.id, opts.sample ?? 0) : await testAction(client, workflowId, hierarchyPath(node, opts.iteration), opts.mock ? "mock" : "test");
|
|
15622
15706
|
return { ...base, ...outcomeOf(row), row };
|
|
15623
15707
|
}
|
|
15624
15708
|
function mergeRows(existing, fresh) {
|
|
@@ -15688,7 +15772,7 @@ function registerTest(program3) {
|
|
|
15688
15772
|
"Run a node in the workflows service and save its sample output; --all runs every node in execution order"
|
|
15689
15773
|
).argument("[nodeId]", "the node to test (its id in softr-workflows.jsonc)").option("--all", "test the trigger and then every action, predecessors first, skipping branches, filters and loops").option("--mock", "mock run instead of a real one (actions only; custom code always runs for real)").option(
|
|
15690
15774
|
"--iteration <n>",
|
|
15691
|
-
"for a node inside a loop:
|
|
15775
|
+
"for a node inside a loop: the iteration of the enclosing loop(s) to use (default 0)",
|
|
15692
15776
|
(value) => Number.parseInt(value, 10)
|
|
15693
15777
|
).option(
|
|
15694
15778
|
"--sample <n>",
|
|
@@ -15744,7 +15828,7 @@ function registerTest(program3) {
|
|
|
15744
15828
|
].join("\n")
|
|
15745
15829
|
);
|
|
15746
15830
|
}
|
|
15747
|
-
|
|
15831
|
+
emitWithVersionNote(options, config, [renderResults(results), notes.join("\n")].filter(Boolean).join("\n\n"), {
|
|
15748
15832
|
workflowId: config.id,
|
|
15749
15833
|
results: results.map(({ row: _row, ...result }) => result),
|
|
15750
15834
|
headersRefreshed: headers.refreshed,
|
|
@@ -15831,7 +15915,7 @@ function registerOutputs(program3) {
|
|
|
15831
15915
|
const row = byId.get(nodeId);
|
|
15832
15916
|
const sample = row?.sampleOutput?.result?.output;
|
|
15833
15917
|
if (!row || sample === void 0 || sample === null) {
|
|
15834
|
-
throw new CliError(`No sample output for "${nodeId}" yet.`, `Run: softr
|
|
15918
|
+
throw new CliError(`No sample output for "${nodeId}" yet.`, `Run: softr workflows test ${nodeId}`);
|
|
15835
15919
|
}
|
|
15836
15920
|
const listing = collectVariables(nodeId, sample, row.outputMetadata ?? []);
|
|
15837
15921
|
emit(options, renderVariables(nodeId, listing.variables, listing.unverified), {
|
|
@@ -15869,7 +15953,7 @@ function registerOutputs(program3) {
|
|
|
15869
15953
|
refreshedHeaders.length ? `Headers refreshed: ${refreshedHeaders.join(", ")}` : "",
|
|
15870
15954
|
problems.length ? `Code files that do not parse (headers not refreshed):
|
|
15871
15955
|
${problems.map((problem) => ` ${problem}`).join("\n")}` : "",
|
|
15872
|
-
'Run "softr
|
|
15956
|
+
'Run "softr workflows outputs <nodeId>" for the variables of one node.'
|
|
15873
15957
|
].filter(Boolean);
|
|
15874
15958
|
emit(options, `${table.toString()}
|
|
15875
15959
|
|
|
@@ -15957,7 +16041,7 @@ function registerPlaceholder(program3) {
|
|
|
15957
16041
|
}
|
|
15958
16042
|
throw new CliError(
|
|
15959
16043
|
`${token} is not backed by test data: ${hint}.`,
|
|
15960
|
-
`Run "softr
|
|
16044
|
+
`Run "softr workflows outputs ${nodeId}" to list every referenceable path.`
|
|
15961
16045
|
);
|
|
15962
16046
|
}
|
|
15963
16047
|
const type = renderJsType(description.type);
|
|
@@ -15979,55 +16063,6 @@ type: ${type}${example3} (${status})`, {
|
|
|
15979
16063
|
import { stdin as stdin2, stdout } from "node:process";
|
|
15980
16064
|
import { createInterface as createInterface2 } from "node:readline/promises";
|
|
15981
16065
|
|
|
15982
|
-
// src/project/placeholder-check.ts
|
|
15983
|
-
var TOKEN = /\{(?:outputs|loopActionGroup)\.[^:{}]+:::[^{}]*\}/g;
|
|
15984
|
-
function* stringsIn(value, path13) {
|
|
15985
|
-
if (typeof value === "string") {
|
|
15986
|
-
yield [path13, value];
|
|
15987
|
-
} else if (Array.isArray(value)) {
|
|
15988
|
-
for (const [index, item] of value.entries()) {
|
|
15989
|
-
yield* stringsIn(item, `${path13}[${index}]`);
|
|
15990
|
-
}
|
|
15991
|
-
} else if (value !== null && typeof value === "object") {
|
|
15992
|
-
for (const [key, item] of Object.entries(value)) {
|
|
15993
|
-
if (key === "actions" || key === "paths") {
|
|
15994
|
-
continue;
|
|
15995
|
-
}
|
|
15996
|
-
yield* stringsIn(item, path13 ? `${path13}.${key}` : key);
|
|
15997
|
-
}
|
|
15998
|
-
}
|
|
15999
|
-
}
|
|
16000
|
-
function checkPlaceholders(config, outputs) {
|
|
16001
|
-
const findings = [];
|
|
16002
|
-
const nodes = [...config.triggers ?? [], ...allActions(config.actions)];
|
|
16003
|
-
for (const node of nodes) {
|
|
16004
|
-
for (const [input, text] of stringsIn(node.inputs ?? {}, "")) {
|
|
16005
|
-
for (const token of new Set(text.match(TOKEN) ?? [])) {
|
|
16006
|
-
if (!parseWholePlaceholder(token)) {
|
|
16007
|
-
continue;
|
|
16008
|
-
}
|
|
16009
|
-
const [description] = describeInputData(
|
|
16010
|
-
{ id: node.id, type: node.type, inputs: { inputData: { probe: token } } },
|
|
16011
|
-
config,
|
|
16012
|
-
outputs
|
|
16013
|
-
);
|
|
16014
|
-
if (!description) {
|
|
16015
|
-
continue;
|
|
16016
|
-
}
|
|
16017
|
-
const status = description.verified ? "verified" : description.type.kind === "unknown" ? "unknown" : "declared";
|
|
16018
|
-
findings.push({
|
|
16019
|
-
nodeId: node.id,
|
|
16020
|
-
input,
|
|
16021
|
-
token,
|
|
16022
|
-
status,
|
|
16023
|
-
...description.note ? { note: description.note } : {}
|
|
16024
|
-
});
|
|
16025
|
-
}
|
|
16026
|
-
}
|
|
16027
|
-
}
|
|
16028
|
-
return findings;
|
|
16029
|
-
}
|
|
16030
|
-
|
|
16031
16066
|
// src/project/readiness.ts
|
|
16032
16067
|
var REASONS = {
|
|
16033
16068
|
REFERENCED_NODE_UNKNOWN: "no node with that id",
|
|
@@ -16074,31 +16109,15 @@ function fromReadiness(readiness) {
|
|
|
16074
16109
|
}
|
|
16075
16110
|
}
|
|
16076
16111
|
return {
|
|
16077
|
-
source: "service",
|
|
16078
16112
|
validationProblems: readiness.validationProblems,
|
|
16079
16113
|
testIssues,
|
|
16080
16114
|
unbacked,
|
|
16081
16115
|
checked: { version: readiness.version, draft: readiness.draft }
|
|
16082
16116
|
};
|
|
16083
16117
|
}
|
|
16084
|
-
function
|
|
16085
|
-
|
|
16086
|
-
return {
|
|
16087
|
-
source: "client",
|
|
16088
|
-
validationProblems: null,
|
|
16089
|
-
testIssues: assessTests(config, rows, lastUpdatedAt),
|
|
16090
|
-
unbacked: checkPlaceholders(config, outputs).filter((finding) => finding.status !== "verified").map((finding) => ({ nodeId: finding.nodeId, token: finding.token, reason: finding.note ?? finding.status })),
|
|
16091
|
-
checked: null
|
|
16092
|
-
};
|
|
16093
|
-
}
|
|
16094
|
-
async function assess(client, workflowId, packed, rows, lastUpdatedAt) {
|
|
16095
|
-
const readiness = await getWorkflowReadiness(client, workflowId);
|
|
16096
|
-
if (readiness) {
|
|
16097
|
-
return fromReadiness(readiness);
|
|
16098
|
-
}
|
|
16099
|
-
return fromClientChecks(packed ?? { actions: [], triggers: [] }, rows, lastUpdatedAt);
|
|
16118
|
+
async function assess(client, workflowId) {
|
|
16119
|
+
return fromReadiness(await getWorkflowReadiness(client, workflowId));
|
|
16100
16120
|
}
|
|
16101
|
-
var CLIENT_SIDE_NOTE = "Checked by this CLI: this deployment of the service has no readiness endpoint (it answered the request the way it answers an unknown route), so the publish validation is not part of this report.";
|
|
16102
16121
|
|
|
16103
16122
|
// src/commands/publish.ts
|
|
16104
16123
|
async function openWorkflow(dir) {
|
|
@@ -16134,15 +16153,15 @@ function registerPublish(program3) {
|
|
|
16134
16153
|
if (state?.syncedHash && definitionHash(packed) !== state.syncedHash) {
|
|
16135
16154
|
throw new CliError("Local edits are not pushed yet.", "Run push first, then publish.");
|
|
16136
16155
|
}
|
|
16137
|
-
const
|
|
16156
|
+
const remote = await getWorkflow(client, id);
|
|
16138
16157
|
if (state && (remote.version !== state.version || remote.draft !== state.draft) && !opts.force) {
|
|
16139
16158
|
throw new CliError(
|
|
16140
16159
|
`The server copy changed since your last sync: it is version ${remote.version}, draft ${remote.draft}; this directory knows version ${state.version}, draft ${state.draft}.`,
|
|
16141
16160
|
'Run "pull --force" to see what would go live, then publish; or "publish --force" to enable it unseen.'
|
|
16142
16161
|
);
|
|
16143
16162
|
}
|
|
16144
|
-
const assessment = await assess(client, id
|
|
16145
|
-
if (assessment.validationProblems
|
|
16163
|
+
const assessment = await assess(client, id);
|
|
16164
|
+
if (assessment.validationProblems.length) {
|
|
16146
16165
|
throw new CliError(
|
|
16147
16166
|
`The service would reject publishing:
|
|
16148
16167
|
${assessment.validationProblems.map((problem) => ` - ${problem}`).join("\n")}`,
|
|
@@ -16152,8 +16171,7 @@ ${assessment.validationProblems.map((problem) => ` - ${problem}`).join("\n")}`,
|
|
|
16152
16171
|
const issues = issuesOf(assessment, config);
|
|
16153
16172
|
if (issues.length && !opts.force) {
|
|
16154
16173
|
const text = `The workflow is not fully tested:
|
|
16155
|
-
${renderIssues(issues)}
|
|
16156
|
-
${CLIENT_SIDE_NOTE}` : ""}`;
|
|
16174
|
+
${renderIssues(issues)}`;
|
|
16157
16175
|
if (stdin2.isTTY && stdout.isTTY) {
|
|
16158
16176
|
const rl = createInterface2({ input: stdin2, output: stdout });
|
|
16159
16177
|
let answer;
|
|
@@ -16168,7 +16186,7 @@ Publish anyway? [y/N] `);
|
|
|
16168
16186
|
return;
|
|
16169
16187
|
}
|
|
16170
16188
|
} else {
|
|
16171
|
-
throw new CliError(text, 'Run "softr
|
|
16189
|
+
throw new CliError(text, 'Run "softr workflows test --all" first, or pass --force to publish anyway.');
|
|
16172
16190
|
}
|
|
16173
16191
|
}
|
|
16174
16192
|
await enableWorkflow(client, id);
|
|
@@ -16190,20 +16208,8 @@ ${target.studioUrl}/workflow/${id}`,
|
|
|
16190
16208
|
const options = globalOptions(command);
|
|
16191
16209
|
const dir = await findWorkflowDir(process.cwd(), opts.dir);
|
|
16192
16210
|
const { id, config, client, target, state } = await openWorkflow(dir);
|
|
16193
|
-
const [remote, live
|
|
16194
|
-
|
|
16195
|
-
getEnabledVersion(client, id),
|
|
16196
|
-
getWorkflowOutputs(client, id)
|
|
16197
|
-
]);
|
|
16198
|
-
let packedForStatus;
|
|
16199
|
-
try {
|
|
16200
|
-
packedForStatus = (await packWorkflow(config, dir)).packed;
|
|
16201
|
-
} catch (error) {
|
|
16202
|
-
if (!(error instanceof CliError)) {
|
|
16203
|
-
throw error;
|
|
16204
|
-
}
|
|
16205
|
-
}
|
|
16206
|
-
const assessment = await assess(client, id, packedForStatus, rows, remote.lastUpdatedAt);
|
|
16211
|
+
const [remote, live] = await Promise.all([getWorkflow(client, id), getEnabledVersion(client, id)]);
|
|
16212
|
+
const assessment = await assess(client, id);
|
|
16207
16213
|
const issues = issuesOf(assessment, config);
|
|
16208
16214
|
let local = "unknown";
|
|
16209
16215
|
let problem;
|
|
@@ -16224,15 +16230,12 @@ ${target.studioUrl}/workflow/${id}`,
|
|
|
16224
16230
|
`Local: ${local}${problem ? ` (${problem})` : ""}${state ? `; last sync version ${state.version}, draft ${state.draft}` : ""}`,
|
|
16225
16231
|
`Server: version ${remote.version}, draft ${remote.draft}${serverMoved ? " (moved since your last sync: pull --force to take it)" : ""}`,
|
|
16226
16232
|
`Live: ${live ? `version ${live.version}` : "not published"}`,
|
|
16227
|
-
|
|
16228
|
-
|
|
16229
|
-
${assessment.validationProblems.map((problem2) => ` - ${problem2}`).join("\n")}` : "the service accepts the workflow for publishing"}`
|
|
16230
|
-
],
|
|
16233
|
+
`Valid: ${assessment.validationProblems.length ? `${assessment.validationProblems.length} problem(s)
|
|
16234
|
+
${assessment.validationProblems.map((problem2) => ` - ${problem2}`).join("\n")}` : "the service accepts the workflow for publishing"}`,
|
|
16231
16235
|
`Tests: ${issues.length ? `${issues.length} issue(s)
|
|
16232
|
-
${renderIssues(issues)}` : "every node tested on current data"}
|
|
16233
|
-
...assessment.source === "client" ? [CLIENT_SIDE_NOTE] : []
|
|
16236
|
+
${renderIssues(issues)}` : "every node tested on current data"}`
|
|
16234
16237
|
];
|
|
16235
|
-
|
|
16238
|
+
emitWithVersionNote(options, config, lines.join("\n"), {
|
|
16236
16239
|
workflowId: id,
|
|
16237
16240
|
title: remote.title,
|
|
16238
16241
|
studioUrl: `${target.studioUrl}/workflow/${id}`,
|
|
@@ -16242,14 +16245,64 @@ ${renderIssues(issues)}` : "every node tested on current data"}`,
|
|
|
16242
16245
|
server: { version: remote.version, draft: remote.draft, moved: serverMoved },
|
|
16243
16246
|
liveVersion: live?.version ?? null,
|
|
16244
16247
|
testIssues: issues,
|
|
16245
|
-
validationProblems: assessment.validationProblems
|
|
16246
|
-
source: assessment.source
|
|
16248
|
+
validationProblems: assessment.validationProblems
|
|
16247
16249
|
});
|
|
16248
16250
|
});
|
|
16249
16251
|
}
|
|
16250
16252
|
|
|
16251
16253
|
// src/commands/push.ts
|
|
16252
16254
|
import path12 from "node:path";
|
|
16255
|
+
|
|
16256
|
+
// src/project/placeholder-check.ts
|
|
16257
|
+
var TOKEN = /\{(?:outputs|loopActionGroup)\.[^:{}]+:::[^{}]*\}/g;
|
|
16258
|
+
function* stringsIn(value, path13) {
|
|
16259
|
+
if (typeof value === "string") {
|
|
16260
|
+
yield [path13, value];
|
|
16261
|
+
} else if (Array.isArray(value)) {
|
|
16262
|
+
for (const [index, item] of value.entries()) {
|
|
16263
|
+
yield* stringsIn(item, `${path13}[${index}]`);
|
|
16264
|
+
}
|
|
16265
|
+
} else if (value !== null && typeof value === "object") {
|
|
16266
|
+
for (const [key, item] of Object.entries(value)) {
|
|
16267
|
+
if (key === "actions" || key === "paths") {
|
|
16268
|
+
continue;
|
|
16269
|
+
}
|
|
16270
|
+
yield* stringsIn(item, path13 ? `${path13}.${key}` : key);
|
|
16271
|
+
}
|
|
16272
|
+
}
|
|
16273
|
+
}
|
|
16274
|
+
function checkPlaceholders(config, outputs) {
|
|
16275
|
+
const findings = [];
|
|
16276
|
+
const nodes = [...config.triggers ?? [], ...allActions(config.actions)];
|
|
16277
|
+
for (const node of nodes) {
|
|
16278
|
+
for (const [input, text] of stringsIn(node.inputs ?? {}, "")) {
|
|
16279
|
+
for (const token of new Set(text.match(TOKEN) ?? [])) {
|
|
16280
|
+
if (!parseWholePlaceholder(token)) {
|
|
16281
|
+
continue;
|
|
16282
|
+
}
|
|
16283
|
+
const [description] = describeInputData(
|
|
16284
|
+
{ id: node.id, type: node.type, inputs: { inputData: { probe: token } } },
|
|
16285
|
+
config,
|
|
16286
|
+
outputs
|
|
16287
|
+
);
|
|
16288
|
+
if (!description) {
|
|
16289
|
+
continue;
|
|
16290
|
+
}
|
|
16291
|
+
const status = description.verified ? "verified" : description.type.kind === "unknown" ? "unknown" : "declared";
|
|
16292
|
+
findings.push({
|
|
16293
|
+
nodeId: node.id,
|
|
16294
|
+
input,
|
|
16295
|
+
token,
|
|
16296
|
+
status,
|
|
16297
|
+
...description.note ? { note: description.note } : {}
|
|
16298
|
+
});
|
|
16299
|
+
}
|
|
16300
|
+
}
|
|
16301
|
+
}
|
|
16302
|
+
return findings;
|
|
16303
|
+
}
|
|
16304
|
+
|
|
16305
|
+
// src/commands/push.ts
|
|
16253
16306
|
function registerPush(program3) {
|
|
16254
16307
|
program3.command("push").description("Inline @@ file references and code files, then save the workflow as a new draft").option("--dir <path>", "workflow directory (default: the current directory)").option("--dry-run", "print the request body instead of sending it").option("--force", "push even when nothing changed locally or the server copy changed since the last sync").action(async (opts, command) => {
|
|
16255
16308
|
const options = globalOptions(command);
|
|
@@ -16308,8 +16361,9 @@ function registerPush(program3) {
|
|
|
16308
16361
|
syncedHash: hash
|
|
16309
16362
|
});
|
|
16310
16363
|
const studioUrl = `${target.studioUrl}/workflow/${config.id}`;
|
|
16311
|
-
|
|
16364
|
+
emitWithVersionNote(
|
|
16312
16365
|
options,
|
|
16366
|
+
config,
|
|
16313
16367
|
`Pushed "${packed.title}": version ${result.version}, draft ${result.draft}` + (inlined.length ? ` (${inlined.length} file${inlined.length === 1 ? "" : "s"} inlined)` : "") + `.
|
|
16314
16368
|
${studioUrl}` + (unbacked.length ? `
|
|
16315
16369
|
Warning: ${unbacked.length} placeholder(s) not backed by test data:
|
|
@@ -16347,7 +16401,7 @@ function renderList() {
|
|
|
16347
16401
|
return [
|
|
16348
16402
|
table.toString(),
|
|
16349
16403
|
"This CLI offers only these node types on purpose: code-first workflows use custom code for everything else.",
|
|
16350
|
-
'Run "softr
|
|
16404
|
+
'Run "softr workflows spec <TYPE>" for the inputs, output and an example of one type.'
|
|
16351
16405
|
].join("\n");
|
|
16352
16406
|
}
|
|
16353
16407
|
function integrationIdFields(entry) {
|
|
@@ -16382,7 +16436,7 @@ ${table.toString()}`);
|
|
|
16382
16436
|
const idFields = integrationIdFields(entry);
|
|
16383
16437
|
if (idFields.length) {
|
|
16384
16438
|
blocks.push(
|
|
16385
|
-
`${idFields.join(", ")}: run "softr
|
|
16439
|
+
`${idFields.join(", ")}: run "softr integrations" for the ids of the workspace's connected integrations.`
|
|
16386
16440
|
);
|
|
16387
16441
|
}
|
|
16388
16442
|
} else {
|
|
@@ -16424,6 +16478,75 @@ function registerSpec(program3) {
|
|
|
16424
16478
|
});
|
|
16425
16479
|
}
|
|
16426
16480
|
|
|
16481
|
+
// src/commands/upgrade.ts
|
|
16482
|
+
function registerUpgrade(program3) {
|
|
16483
|
+
program3.command("upgrade").description(
|
|
16484
|
+
"Move nodes on an older version of their type to the current one: CUSTOM_CODE 1.0.0 and 1.1.0 become 1.2.0 (the integrations list is added, the code file keeps its body and gets the new header)"
|
|
16485
|
+
).argument("[nodeIds...]", "the nodes to upgrade (default: every node the CLI can upgrade)").option("--dir <path>", "workflow directory (default: the current directory)").action(async (nodeIds, opts, command) => {
|
|
16486
|
+
const options = globalOptions(command);
|
|
16487
|
+
const dir = await findWorkflowDir(process.cwd(), opts.dir);
|
|
16488
|
+
const config = await readConfig(dir);
|
|
16489
|
+
const outdated = outdatedNodes(config);
|
|
16490
|
+
for (const id of nodeIds) {
|
|
16491
|
+
if (!locateNode(config, id)) {
|
|
16492
|
+
throw new CliError(
|
|
16493
|
+
`No node "${id}" in this workflow.`,
|
|
16494
|
+
'Node ids are the "id" values in softr-workflows.jsonc.'
|
|
16495
|
+
);
|
|
16496
|
+
}
|
|
16497
|
+
}
|
|
16498
|
+
const wanted = nodeIds.length ? outdated.filter((node) => nodeIds.includes(node.id)) : outdated;
|
|
16499
|
+
const current2 = nodeIds.filter((id) => !outdated.some((node) => node.id === id));
|
|
16500
|
+
const upgraded = [];
|
|
16501
|
+
const manual = [];
|
|
16502
|
+
const edits = [];
|
|
16503
|
+
for (const node of wanted) {
|
|
16504
|
+
const location = locateNode(config, node.id);
|
|
16505
|
+
const next = location ? upgradeOf(location.node) : void 0;
|
|
16506
|
+
if (!location || !next) {
|
|
16507
|
+
manual.push(node);
|
|
16508
|
+
continue;
|
|
16509
|
+
}
|
|
16510
|
+
edits.push({ path: location.path, value: next });
|
|
16511
|
+
upgraded.push(node);
|
|
16512
|
+
}
|
|
16513
|
+
if (edits.length) {
|
|
16514
|
+
await editConfig(dir, edits);
|
|
16515
|
+
}
|
|
16516
|
+
const headers = edits.length ? await refreshCodeHeaders(
|
|
16517
|
+
await readConfig(dir),
|
|
16518
|
+
dir,
|
|
16519
|
+
indexOutputs(await readStateFile(dir, OUTPUTS_FILE) ?? [])
|
|
16520
|
+
) : { refreshed: [], problems: [] };
|
|
16521
|
+
if (!wanted.length && manual.length === 0 && current2.length === 0) {
|
|
16522
|
+
emit(options, "Every node is on the current version of its type.", { upgraded: [], manual: [], current: [] });
|
|
16523
|
+
return;
|
|
16524
|
+
}
|
|
16525
|
+
if (headers.problems.length) {
|
|
16526
|
+
throw new CliError(
|
|
16527
|
+
`Upgraded, but a code file could not get its new header:
|
|
16528
|
+
${headers.problems.map((problem) => ` ${problem}`).join("\n")}`,
|
|
16529
|
+
'Fix the file, then run "pull" to regenerate its header.'
|
|
16530
|
+
);
|
|
16531
|
+
}
|
|
16532
|
+
const lines = [
|
|
16533
|
+
...upgraded.map((node) => `Upgraded ${node.id}: ${node.type} ${node.version} \u2192 ${node.current}.`),
|
|
16534
|
+
...manual.map(
|
|
16535
|
+
(node) => `No automatic upgrade for ${node.id}: ${node.type} ${node.version} \u2192 ${node.current}; compare its inputs with "spec ${node.type}" and set the version by hand.`
|
|
16536
|
+
),
|
|
16537
|
+
...current2.map((id) => `${id} is already on the current version.`),
|
|
16538
|
+
headers.refreshed.length ? `Headers refreshed: ${headers.refreshed.join(", ")}` : "",
|
|
16539
|
+
upgraded.length ? "Then: softr workflows push, softr workflows test --all." : ""
|
|
16540
|
+
].filter(Boolean);
|
|
16541
|
+
emit(options, lines.join("\n"), {
|
|
16542
|
+
upgraded: upgraded.map((node) => ({ id: node.id, type: node.type, from: node.version, to: node.current })),
|
|
16543
|
+
manual: manual.map((node) => ({ id: node.id, type: node.type, from: node.version, to: node.current })),
|
|
16544
|
+
current: current2,
|
|
16545
|
+
headersRefreshed: headers.refreshed
|
|
16546
|
+
});
|
|
16547
|
+
});
|
|
16548
|
+
}
|
|
16549
|
+
|
|
16427
16550
|
// src/commands/verify.ts
|
|
16428
16551
|
function section(title, lines, okText) {
|
|
16429
16552
|
return { title, ok: lines.length === 0, lines: lines.length ? lines : [okText] };
|
|
@@ -16479,7 +16602,7 @@ function registerVerify(program3) {
|
|
|
16479
16602
|
`in sync with the server (version ${remote.version}, draft ${remote.draft})`
|
|
16480
16603
|
);
|
|
16481
16604
|
let rows = await getWorkflowOutputs(client, config.id);
|
|
16482
|
-
let assessment = await assess(client, config.id
|
|
16605
|
+
let assessment = await assess(client, config.id);
|
|
16483
16606
|
let ran = [];
|
|
16484
16607
|
if (opts.testMissing && sync.ok && files.ok) {
|
|
16485
16608
|
const wanted = new Set(
|
|
@@ -16489,23 +16612,19 @@ function registerVerify(program3) {
|
|
|
16489
16612
|
if (nodes.length) {
|
|
16490
16613
|
ran = await runNodeTests(client, config.id, nodes, {}, true);
|
|
16491
16614
|
rows = await getWorkflowOutputs(client, config.id);
|
|
16492
|
-
assessment = await assess(client, config.id
|
|
16615
|
+
assessment = await assess(client, config.id);
|
|
16493
16616
|
}
|
|
16494
16617
|
}
|
|
16495
16618
|
await writeStateFile(dir, OUTPUTS_FILE, mergeRows([], rows));
|
|
16496
16619
|
const headers = await refreshCodeHeaders(config, dir, indexOutputs(rows));
|
|
16497
|
-
if (assessment.checked
|
|
16620
|
+
if (assessment.checked.version !== remote.version || assessment.checked.draft !== remote.draft) {
|
|
16498
16621
|
syncLines.push(
|
|
16499
16622
|
`the service assessed version ${assessment.checked.version}, draft ${assessment.checked.draft}, but the workflow is already at version ${remote.version}, draft ${remote.draft} (run verify again)`
|
|
16500
16623
|
);
|
|
16501
16624
|
}
|
|
16502
16625
|
const sections = [files, sync];
|
|
16503
|
-
if (assessment.validationProblems !== null) {
|
|
16504
|
-
sections.push(
|
|
16505
|
-
section("Validation", assessment.validationProblems, "the service accepts the workflow for publishing")
|
|
16506
|
-
);
|
|
16507
|
-
}
|
|
16508
16626
|
sections.push(
|
|
16627
|
+
section("Validation", assessment.validationProblems, "the service accepts the workflow for publishing"),
|
|
16509
16628
|
section(
|
|
16510
16629
|
"Tests",
|
|
16511
16630
|
assessment.testIssues.map((issue) => `${issue.id} (${issue.type}): ${issue.detail}`),
|
|
@@ -16525,17 +16644,15 @@ function registerVerify(program3) {
|
|
|
16525
16644
|
(item) => `${item.ok ? "OK " : "NOT"} ${item.title}
|
|
16526
16645
|
${item.lines.map((line) => ` ${line}`).join("\n")}`
|
|
16527
16646
|
),
|
|
16528
|
-
assessment.source === "client" ? CLIENT_SIDE_NOTE : "",
|
|
16529
16647
|
headers.refreshed.length ? `Headers refreshed: ${headers.refreshed.join(", ")}` : "",
|
|
16530
16648
|
ready ? "Ready to publish." : `Not ready to publish: ${sections.filter((item) => !item.ok).length} section(s) need attention.`
|
|
16531
16649
|
].filter(Boolean).join("\n\n");
|
|
16532
|
-
|
|
16650
|
+
emitWithVersionNote(options, config, human, {
|
|
16533
16651
|
workflowId: config.id,
|
|
16534
16652
|
ready,
|
|
16535
|
-
source: assessment.source,
|
|
16536
16653
|
files: { ok: files.ok, problems: fileProblems },
|
|
16537
16654
|
sync: { ok: sync.ok, problems: syncLines, server: { version: remote.version, draft: remote.draft } },
|
|
16538
|
-
validation:
|
|
16655
|
+
validation: { ok: assessment.validationProblems.length === 0, problems: assessment.validationProblems },
|
|
16539
16656
|
tests: {
|
|
16540
16657
|
ok: assessment.testIssues.length === 0,
|
|
16541
16658
|
issues: assessment.testIssues,
|
|
@@ -16549,10 +16666,36 @@ ${item.lines.map((line) => ` ${line}`).join("\n")}`
|
|
|
16549
16666
|
});
|
|
16550
16667
|
}
|
|
16551
16668
|
|
|
16669
|
+
// src/commands/workspaces.ts
|
|
16670
|
+
var import_cli_table36 = __toESM(require_cli_table3(), 1);
|
|
16671
|
+
function registerWorkspaces(program3) {
|
|
16672
|
+
program3.command("workspaces").description("The workspaces the stored token reaches, with your role in each").action(async (_opts, command) => {
|
|
16673
|
+
const options = globalOptions(command);
|
|
16674
|
+
const target = resolveTarget();
|
|
16675
|
+
const client = await authenticatedClient(target);
|
|
16676
|
+
const workspaces = await listWorkspaces(client);
|
|
16677
|
+
const table = new import_cli_table36.default({
|
|
16678
|
+
head: ["ID", "Name", "Role", "Default"],
|
|
16679
|
+
colWidths: [38, 34, 14, 9],
|
|
16680
|
+
wordWrap: true,
|
|
16681
|
+
style: { head: [], border: [], "padding-left": 1, "padding-right": 1 }
|
|
16682
|
+
});
|
|
16683
|
+
for (const workspace of workspaces) {
|
|
16684
|
+
table.push([workspace.id, workspace.name, workspace.userRole ?? "", workspace.isDefault ? "yes" : ""]);
|
|
16685
|
+
}
|
|
16686
|
+
emit(
|
|
16687
|
+
options,
|
|
16688
|
+
workspaces.length ? `${table.toString()}
|
|
16689
|
+
Pass --workspace <id> to a command, or set SOFTR_WORKSPACE_ID, to work in one of them.` : "The token reaches no workspace.",
|
|
16690
|
+
{ workspaces }
|
|
16691
|
+
);
|
|
16692
|
+
});
|
|
16693
|
+
}
|
|
16694
|
+
|
|
16552
16695
|
// package.json
|
|
16553
16696
|
var package_default = {
|
|
16554
16697
|
name: "@softr/cli",
|
|
16555
|
-
version: "0.
|
|
16698
|
+
version: "0.1.1-se21216.10",
|
|
16556
16699
|
description: "The Softr command line: build, test and publish Softr workflows from local files; more Softr APIs to come",
|
|
16557
16700
|
keywords: [
|
|
16558
16701
|
"softr",
|
|
@@ -16562,8 +16705,7 @@ var package_default = {
|
|
|
16562
16705
|
license: "UNLICENSED",
|
|
16563
16706
|
type: "module",
|
|
16564
16707
|
bin: {
|
|
16565
|
-
softr: "dist/cli.mjs"
|
|
16566
|
-
"softr-workflows": "dist/cli.mjs"
|
|
16708
|
+
softr: "dist/cli.mjs"
|
|
16567
16709
|
},
|
|
16568
16710
|
files: [
|
|
16569
16711
|
"dist"
|
|
@@ -16580,7 +16722,7 @@ var package_default = {
|
|
|
16580
16722
|
format: "prettier --write .",
|
|
16581
16723
|
"format:check": "prettier --check .",
|
|
16582
16724
|
check: "npm run typecheck && npm run format:check && npm test && npm run build",
|
|
16583
|
-
"check:skill": "npx -y skills-ref@0.1.5 validate skills/softr-workflows"
|
|
16725
|
+
"check:skill": "npx -y skills-ref@0.1.5 validate skills/softr-cli && npx -y skills-ref@0.1.5 validate skills/softr-workflows"
|
|
16584
16726
|
},
|
|
16585
16727
|
publishConfig: {
|
|
16586
16728
|
access: "public"
|
|
@@ -16603,20 +16745,23 @@ var package_default = {
|
|
|
16603
16745
|
};
|
|
16604
16746
|
|
|
16605
16747
|
// src/cli.ts
|
|
16606
|
-
var program2 = new Command().name("softr").description("
|
|
16748
|
+
var program2 = new Command().name("softr").description("The Softr command line: workflows today, more of Softr to come").version(package_default.version).option("--json", "print results as JSON");
|
|
16607
16749
|
registerLogin(program2);
|
|
16608
|
-
|
|
16750
|
+
registerWorkspaces(program2);
|
|
16609
16751
|
registerIntegrations(program2);
|
|
16610
|
-
|
|
16611
|
-
|
|
16612
|
-
|
|
16613
|
-
|
|
16614
|
-
|
|
16615
|
-
|
|
16616
|
-
|
|
16617
|
-
|
|
16618
|
-
|
|
16619
|
-
|
|
16620
|
-
|
|
16621
|
-
|
|
16752
|
+
var workflows = program2.command("workflows").description("Build, test and publish Softr Workflows from local files (one directory per workflow)");
|
|
16753
|
+
registerList(workflows);
|
|
16754
|
+
registerPull(workflows);
|
|
16755
|
+
registerPush(workflows);
|
|
16756
|
+
registerTest(workflows);
|
|
16757
|
+
registerVerify(workflows);
|
|
16758
|
+
registerOutputs(workflows);
|
|
16759
|
+
registerPlaceholder(workflows);
|
|
16760
|
+
registerInit(workflows);
|
|
16761
|
+
registerAdd(workflows);
|
|
16762
|
+
registerEdit(workflows);
|
|
16763
|
+
registerUpgrade(workflows);
|
|
16764
|
+
registerPublish(workflows);
|
|
16765
|
+
registerSpec(workflows);
|
|
16766
|
+
registerSchema(workflows);
|
|
16622
16767
|
program2.parseAsync(process.argv).catch(reportError);
|