@softr/cli 0.0.1 → 0.1.1-se21216.6
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 +399 -259
- 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
|
{
|
|
@@ -14291,9 +14401,8 @@ function listWorkflows(client, workspaceId) {
|
|
|
14291
14401
|
const query = new URLSearchParams({ workspaceId, 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;
|
|
@@ -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}
|
|
@@ -14689,11 +14793,11 @@ async function emitWorkflowList(options, client, workspaceId, human) {
|
|
|
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)").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"]);
|
|
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,7 +15265,10 @@ 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(
|
|
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("--trigger <TYPE>", 'trigger type from "spec" (default WEBHOOK)', "WEBHOOK").option("--dir <path>", "directory to create (default: ./<title-slug>)").action(async (title, opts, command) => {
|
|
15162
15272
|
const options = globalOptions(command);
|
|
15163
15273
|
const cwd = process.cwd();
|
|
15164
15274
|
const dir = path10.resolve(cwd, opts.dir ?? slug(title));
|
|
@@ -15168,7 +15278,7 @@ function registerInit(program3) {
|
|
|
15168
15278
|
const trigger = findCatalogEntry(opts.trigger, "trigger");
|
|
15169
15279
|
const target = resolveTarget();
|
|
15170
15280
|
const client = await authenticatedClient(target);
|
|
15171
|
-
const workspaceId = await resolveWorkspace(client, opts.workspace);
|
|
15281
|
+
const workspaceId = await resolveWorkspace(client, opts.workspace ?? process.env["SOFTR_WORKSPACE_ID"]);
|
|
15172
15282
|
const created = await createWorkflow(client, {
|
|
15173
15283
|
workspaceId,
|
|
15174
15284
|
applicationId: null,
|
|
@@ -15198,7 +15308,7 @@ function registerInit(program3) {
|
|
|
15198
15308
|
`Created "${title}" (${created.id}) with a ${trigger.type} trigger "${TRIGGER_ID}" in workspace ${workspaceId}.
|
|
15199
15309
|
${path10.relative(cwd, configFile)}
|
|
15200
15310
|
${studioUrl}
|
|
15201
|
-
Next: softr
|
|
15311
|
+
Next: softr workflows add <TYPE> --dir ${path10.relative(cwd, dir) || "."} (see "spec" for the types), then push and test.`,
|
|
15202
15312
|
{
|
|
15203
15313
|
workflowId: created.id,
|
|
15204
15314
|
workspaceId,
|
|
@@ -15263,12 +15373,15 @@ async function workspaceFor(client, requested, workflowDir) {
|
|
|
15263
15373
|
return resolveWorkspace(client, void 0);
|
|
15264
15374
|
}
|
|
15265
15375
|
function registerIntegrations(program3) {
|
|
15266
|
-
program3.command("integrations").description("List the workspace's connected integrations, whose ids go into integrationId inputs").option(
|
|
15376
|
+
program3.command("integrations").description("List the workspace's connected integrations, whose ids go into integrationId inputs").option(
|
|
15377
|
+
"--workspace <id>",
|
|
15378
|
+
"workspace to list (default: $SOFTR_WORKSPACE_ID, else the workflow directory's, else your only or default workspace)"
|
|
15379
|
+
).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
15380
|
const options = globalOptions(command);
|
|
15268
15381
|
const workflowDir = await workflowDirOf(process.cwd(), opts.dir);
|
|
15269
15382
|
const target = resolveTarget(await readState(workflowDir ?? process.cwd()));
|
|
15270
15383
|
const client = await authenticatedClient(target);
|
|
15271
|
-
const workspaceId = await workspaceFor(client, opts.workspace, workflowDir);
|
|
15384
|
+
const workspaceId = await workspaceFor(client, opts.workspace ?? process.env["SOFTR_WORKSPACE_ID"], workflowDir);
|
|
15272
15385
|
const wanted = opts.type?.toUpperCase();
|
|
15273
15386
|
const rows = (await listWorkspaceIntegrations(client, workspaceId)).map(toRow2).filter((row) => !wanted || row.type === wanted).sort(byTypeThenName);
|
|
15274
15387
|
emit(options, `Workspace ${workspaceId}${wanted ? `, type ${wanted}` : ""}:
|
|
@@ -15322,7 +15435,7 @@ async function interactiveLogin(target) {
|
|
|
15322
15435
|
if (answer === "2") {
|
|
15323
15436
|
const token = (await rl.question("Personal API token: ")).trim();
|
|
15324
15437
|
if (!token) {
|
|
15325
|
-
throw new CliError("No token given.", 'Run "softr
|
|
15438
|
+
throw new CliError("No token given.", 'Run "softr login" again.');
|
|
15326
15439
|
}
|
|
15327
15440
|
return pastedToken(token);
|
|
15328
15441
|
}
|
|
@@ -15350,7 +15463,7 @@ function waitForCallback(expectedState) {
|
|
|
15350
15463
|
}
|
|
15351
15464
|
const finish = (status, text) => {
|
|
15352
15465
|
response.writeHead(status, { "Content-Type": "text/html; charset=utf-8", Connection: "close" });
|
|
15353
|
-
response.end(`<!doctype html><title>softr
|
|
15466
|
+
response.end(`<!doctype html><title>softr</title><p>${text} You can close this tab.</p>`);
|
|
15354
15467
|
server.close();
|
|
15355
15468
|
};
|
|
15356
15469
|
const error = url.searchParams.get("error");
|
|
@@ -15377,9 +15490,7 @@ function waitForCallback(expectedState) {
|
|
|
15377
15490
|
}
|
|
15378
15491
|
const timer = setTimeout(() => {
|
|
15379
15492
|
server.close();
|
|
15380
|
-
settle?.reject(
|
|
15381
|
-
new CliError("No answer from the browser within 5 minutes.", 'Run "softr-workflows login" again.')
|
|
15382
|
-
);
|
|
15493
|
+
settle?.reject(new CliError("No answer from the browser within 5 minutes.", 'Run "softr login" again.'));
|
|
15383
15494
|
}, CALLBACK_TIMEOUT_MS);
|
|
15384
15495
|
timer.unref();
|
|
15385
15496
|
resolveServer({ redirectUri: `http://127.0.0.1:${address.port}/callback`, result });
|
|
@@ -15436,6 +15547,11 @@ function registerLogin(program3) {
|
|
|
15436
15547
|
).option("--no-browser", "browser login, but print the URL instead of opening it (for remote shells and agents)").action(async (opts, command) => {
|
|
15437
15548
|
const options = globalOptions(command);
|
|
15438
15549
|
const target = resolveTarget();
|
|
15550
|
+
if (process.env[AUTH_ENV_VAR] === "proxy") {
|
|
15551
|
+
throw new CliError(
|
|
15552
|
+
`Login is not needed: the target authenticates the requests itself ($${AUTH_ENV_VAR}=proxy).`
|
|
15553
|
+
);
|
|
15554
|
+
}
|
|
15439
15555
|
const token = (opts.token ?? process.env[TOKEN_ENV_VAR] ?? "").trim();
|
|
15440
15556
|
const saved = token ? pastedToken(token) : opts.browser ? await interactiveLogin(target) : await browserLogin(target, true);
|
|
15441
15557
|
const identity = await identify({ target, token: saved.token, kind: saved.kind });
|
|
@@ -15455,6 +15571,14 @@ function registerLogin(program3) {
|
|
|
15455
15571
|
const options = globalOptions(command);
|
|
15456
15572
|
const target = resolveTarget();
|
|
15457
15573
|
const client = await authenticatedClient(target);
|
|
15574
|
+
if (client.kind === "proxy") {
|
|
15575
|
+
emit(
|
|
15576
|
+
options,
|
|
15577
|
+
`Requests to ${targetHost(target)} carry no token: the target authenticates them itself ($${AUTH_ENV_VAR}=proxy).`,
|
|
15578
|
+
{ kind: "proxy", host: targetHost(target) }
|
|
15579
|
+
);
|
|
15580
|
+
return;
|
|
15581
|
+
}
|
|
15458
15582
|
const identity = await identify(client);
|
|
15459
15583
|
const stored = process.env[TOKEN_ENV_VAR] ? void 0 : await storedToken(target);
|
|
15460
15584
|
const expiry = stored?.expiresAt ? ` Expires ${stored.expiresAt.slice(0, 16).replace("T", " ")} UTC.` : "";
|
|
@@ -15484,51 +15608,6 @@ function outdatedReferences(row) {
|
|
|
15484
15608
|
const outdated = row?.sampleOutput?.outdatedDependencies;
|
|
15485
15609
|
return outdated && Object.keys(outdated).length ? outdated : {};
|
|
15486
15610
|
}
|
|
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
15611
|
function renderIssues(issues) {
|
|
15533
15612
|
return issues.map((issue) => ` - ${issue.id} (${issue.type}): ${issue.detail}`).join("\n");
|
|
15534
15613
|
}
|
|
@@ -15618,7 +15697,7 @@ async function runOne(client, workflowId, node, opts) {
|
|
|
15618
15697
|
if (!node.testable) {
|
|
15619
15698
|
return { ...base, outcome: "skipped", detail: "branches, filters and loops have no output of their own" };
|
|
15620
15699
|
}
|
|
15621
|
-
const row = node.kind === "trigger" ? await runTrigger(client, workflowId, node.id, opts.sample ?? 0) : await testAction(client, workflowId, node.
|
|
15700
|
+
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
15701
|
return { ...base, ...outcomeOf(row), row };
|
|
15623
15702
|
}
|
|
15624
15703
|
function mergeRows(existing, fresh) {
|
|
@@ -15688,7 +15767,7 @@ function registerTest(program3) {
|
|
|
15688
15767
|
"Run a node in the workflows service and save its sample output; --all runs every node in execution order"
|
|
15689
15768
|
).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
15769
|
"--iteration <n>",
|
|
15691
|
-
"for a node inside a loop:
|
|
15770
|
+
"for a node inside a loop: the iteration of the enclosing loop(s) to use (default 0)",
|
|
15692
15771
|
(value) => Number.parseInt(value, 10)
|
|
15693
15772
|
).option(
|
|
15694
15773
|
"--sample <n>",
|
|
@@ -15744,7 +15823,7 @@ function registerTest(program3) {
|
|
|
15744
15823
|
].join("\n")
|
|
15745
15824
|
);
|
|
15746
15825
|
}
|
|
15747
|
-
|
|
15826
|
+
emitWithVersionNote(options, config, [renderResults(results), notes.join("\n")].filter(Boolean).join("\n\n"), {
|
|
15748
15827
|
workflowId: config.id,
|
|
15749
15828
|
results: results.map(({ row: _row, ...result }) => result),
|
|
15750
15829
|
headersRefreshed: headers.refreshed,
|
|
@@ -15831,7 +15910,7 @@ function registerOutputs(program3) {
|
|
|
15831
15910
|
const row = byId.get(nodeId);
|
|
15832
15911
|
const sample = row?.sampleOutput?.result?.output;
|
|
15833
15912
|
if (!row || sample === void 0 || sample === null) {
|
|
15834
|
-
throw new CliError(`No sample output for "${nodeId}" yet.`, `Run: softr
|
|
15913
|
+
throw new CliError(`No sample output for "${nodeId}" yet.`, `Run: softr workflows test ${nodeId}`);
|
|
15835
15914
|
}
|
|
15836
15915
|
const listing = collectVariables(nodeId, sample, row.outputMetadata ?? []);
|
|
15837
15916
|
emit(options, renderVariables(nodeId, listing.variables, listing.unverified), {
|
|
@@ -15869,7 +15948,7 @@ function registerOutputs(program3) {
|
|
|
15869
15948
|
refreshedHeaders.length ? `Headers refreshed: ${refreshedHeaders.join(", ")}` : "",
|
|
15870
15949
|
problems.length ? `Code files that do not parse (headers not refreshed):
|
|
15871
15950
|
${problems.map((problem) => ` ${problem}`).join("\n")}` : "",
|
|
15872
|
-
'Run "softr
|
|
15951
|
+
'Run "softr workflows outputs <nodeId>" for the variables of one node.'
|
|
15873
15952
|
].filter(Boolean);
|
|
15874
15953
|
emit(options, `${table.toString()}
|
|
15875
15954
|
|
|
@@ -15957,7 +16036,7 @@ function registerPlaceholder(program3) {
|
|
|
15957
16036
|
}
|
|
15958
16037
|
throw new CliError(
|
|
15959
16038
|
`${token} is not backed by test data: ${hint}.`,
|
|
15960
|
-
`Run "softr
|
|
16039
|
+
`Run "softr workflows outputs ${nodeId}" to list every referenceable path.`
|
|
15961
16040
|
);
|
|
15962
16041
|
}
|
|
15963
16042
|
const type = renderJsType(description.type);
|
|
@@ -15979,55 +16058,6 @@ type: ${type}${example3} (${status})`, {
|
|
|
15979
16058
|
import { stdin as stdin2, stdout } from "node:process";
|
|
15980
16059
|
import { createInterface as createInterface2 } from "node:readline/promises";
|
|
15981
16060
|
|
|
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
16061
|
// src/project/readiness.ts
|
|
16032
16062
|
var REASONS = {
|
|
16033
16063
|
REFERENCED_NODE_UNKNOWN: "no node with that id",
|
|
@@ -16074,31 +16104,15 @@ function fromReadiness(readiness) {
|
|
|
16074
16104
|
}
|
|
16075
16105
|
}
|
|
16076
16106
|
return {
|
|
16077
|
-
source: "service",
|
|
16078
16107
|
validationProblems: readiness.validationProblems,
|
|
16079
16108
|
testIssues,
|
|
16080
16109
|
unbacked,
|
|
16081
16110
|
checked: { version: readiness.version, draft: readiness.draft }
|
|
16082
16111
|
};
|
|
16083
16112
|
}
|
|
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);
|
|
16113
|
+
async function assess(client, workflowId) {
|
|
16114
|
+
return fromReadiness(await getWorkflowReadiness(client, workflowId));
|
|
16100
16115
|
}
|
|
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
16116
|
|
|
16103
16117
|
// src/commands/publish.ts
|
|
16104
16118
|
async function openWorkflow(dir) {
|
|
@@ -16134,15 +16148,15 @@ function registerPublish(program3) {
|
|
|
16134
16148
|
if (state?.syncedHash && definitionHash(packed) !== state.syncedHash) {
|
|
16135
16149
|
throw new CliError("Local edits are not pushed yet.", "Run push first, then publish.");
|
|
16136
16150
|
}
|
|
16137
|
-
const
|
|
16151
|
+
const remote = await getWorkflow(client, id);
|
|
16138
16152
|
if (state && (remote.version !== state.version || remote.draft !== state.draft) && !opts.force) {
|
|
16139
16153
|
throw new CliError(
|
|
16140
16154
|
`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
16155
|
'Run "pull --force" to see what would go live, then publish; or "publish --force" to enable it unseen.'
|
|
16142
16156
|
);
|
|
16143
16157
|
}
|
|
16144
|
-
const assessment = await assess(client, id
|
|
16145
|
-
if (assessment.validationProblems
|
|
16158
|
+
const assessment = await assess(client, id);
|
|
16159
|
+
if (assessment.validationProblems.length) {
|
|
16146
16160
|
throw new CliError(
|
|
16147
16161
|
`The service would reject publishing:
|
|
16148
16162
|
${assessment.validationProblems.map((problem) => ` - ${problem}`).join("\n")}`,
|
|
@@ -16152,8 +16166,7 @@ ${assessment.validationProblems.map((problem) => ` - ${problem}`).join("\n")}`,
|
|
|
16152
16166
|
const issues = issuesOf(assessment, config);
|
|
16153
16167
|
if (issues.length && !opts.force) {
|
|
16154
16168
|
const text = `The workflow is not fully tested:
|
|
16155
|
-
${renderIssues(issues)}
|
|
16156
|
-
${CLIENT_SIDE_NOTE}` : ""}`;
|
|
16169
|
+
${renderIssues(issues)}`;
|
|
16157
16170
|
if (stdin2.isTTY && stdout.isTTY) {
|
|
16158
16171
|
const rl = createInterface2({ input: stdin2, output: stdout });
|
|
16159
16172
|
let answer;
|
|
@@ -16168,7 +16181,7 @@ Publish anyway? [y/N] `);
|
|
|
16168
16181
|
return;
|
|
16169
16182
|
}
|
|
16170
16183
|
} else {
|
|
16171
|
-
throw new CliError(text, 'Run "softr
|
|
16184
|
+
throw new CliError(text, 'Run "softr workflows test --all" first, or pass --force to publish anyway.');
|
|
16172
16185
|
}
|
|
16173
16186
|
}
|
|
16174
16187
|
await enableWorkflow(client, id);
|
|
@@ -16190,20 +16203,8 @@ ${target.studioUrl}/workflow/${id}`,
|
|
|
16190
16203
|
const options = globalOptions(command);
|
|
16191
16204
|
const dir = await findWorkflowDir(process.cwd(), opts.dir);
|
|
16192
16205
|
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);
|
|
16206
|
+
const [remote, live] = await Promise.all([getWorkflow(client, id), getEnabledVersion(client, id)]);
|
|
16207
|
+
const assessment = await assess(client, id);
|
|
16207
16208
|
const issues = issuesOf(assessment, config);
|
|
16208
16209
|
let local = "unknown";
|
|
16209
16210
|
let problem;
|
|
@@ -16224,15 +16225,12 @@ ${target.studioUrl}/workflow/${id}`,
|
|
|
16224
16225
|
`Local: ${local}${problem ? ` (${problem})` : ""}${state ? `; last sync version ${state.version}, draft ${state.draft}` : ""}`,
|
|
16225
16226
|
`Server: version ${remote.version}, draft ${remote.draft}${serverMoved ? " (moved since your last sync: pull --force to take it)" : ""}`,
|
|
16226
16227
|
`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
|
-
],
|
|
16228
|
+
`Valid: ${assessment.validationProblems.length ? `${assessment.validationProblems.length} problem(s)
|
|
16229
|
+
${assessment.validationProblems.map((problem2) => ` - ${problem2}`).join("\n")}` : "the service accepts the workflow for publishing"}`,
|
|
16231
16230
|
`Tests: ${issues.length ? `${issues.length} issue(s)
|
|
16232
|
-
${renderIssues(issues)}` : "every node tested on current data"}
|
|
16233
|
-
...assessment.source === "client" ? [CLIENT_SIDE_NOTE] : []
|
|
16231
|
+
${renderIssues(issues)}` : "every node tested on current data"}`
|
|
16234
16232
|
];
|
|
16235
|
-
|
|
16233
|
+
emitWithVersionNote(options, config, lines.join("\n"), {
|
|
16236
16234
|
workflowId: id,
|
|
16237
16235
|
title: remote.title,
|
|
16238
16236
|
studioUrl: `${target.studioUrl}/workflow/${id}`,
|
|
@@ -16242,14 +16240,64 @@ ${renderIssues(issues)}` : "every node tested on current data"}`,
|
|
|
16242
16240
|
server: { version: remote.version, draft: remote.draft, moved: serverMoved },
|
|
16243
16241
|
liveVersion: live?.version ?? null,
|
|
16244
16242
|
testIssues: issues,
|
|
16245
|
-
validationProblems: assessment.validationProblems
|
|
16246
|
-
source: assessment.source
|
|
16243
|
+
validationProblems: assessment.validationProblems
|
|
16247
16244
|
});
|
|
16248
16245
|
});
|
|
16249
16246
|
}
|
|
16250
16247
|
|
|
16251
16248
|
// src/commands/push.ts
|
|
16252
16249
|
import path12 from "node:path";
|
|
16250
|
+
|
|
16251
|
+
// src/project/placeholder-check.ts
|
|
16252
|
+
var TOKEN = /\{(?:outputs|loopActionGroup)\.[^:{}]+:::[^{}]*\}/g;
|
|
16253
|
+
function* stringsIn(value, path13) {
|
|
16254
|
+
if (typeof value === "string") {
|
|
16255
|
+
yield [path13, value];
|
|
16256
|
+
} else if (Array.isArray(value)) {
|
|
16257
|
+
for (const [index, item] of value.entries()) {
|
|
16258
|
+
yield* stringsIn(item, `${path13}[${index}]`);
|
|
16259
|
+
}
|
|
16260
|
+
} else if (value !== null && typeof value === "object") {
|
|
16261
|
+
for (const [key, item] of Object.entries(value)) {
|
|
16262
|
+
if (key === "actions" || key === "paths") {
|
|
16263
|
+
continue;
|
|
16264
|
+
}
|
|
16265
|
+
yield* stringsIn(item, path13 ? `${path13}.${key}` : key);
|
|
16266
|
+
}
|
|
16267
|
+
}
|
|
16268
|
+
}
|
|
16269
|
+
function checkPlaceholders(config, outputs) {
|
|
16270
|
+
const findings = [];
|
|
16271
|
+
const nodes = [...config.triggers ?? [], ...allActions(config.actions)];
|
|
16272
|
+
for (const node of nodes) {
|
|
16273
|
+
for (const [input, text] of stringsIn(node.inputs ?? {}, "")) {
|
|
16274
|
+
for (const token of new Set(text.match(TOKEN) ?? [])) {
|
|
16275
|
+
if (!parseWholePlaceholder(token)) {
|
|
16276
|
+
continue;
|
|
16277
|
+
}
|
|
16278
|
+
const [description] = describeInputData(
|
|
16279
|
+
{ id: node.id, type: node.type, inputs: { inputData: { probe: token } } },
|
|
16280
|
+
config,
|
|
16281
|
+
outputs
|
|
16282
|
+
);
|
|
16283
|
+
if (!description) {
|
|
16284
|
+
continue;
|
|
16285
|
+
}
|
|
16286
|
+
const status = description.verified ? "verified" : description.type.kind === "unknown" ? "unknown" : "declared";
|
|
16287
|
+
findings.push({
|
|
16288
|
+
nodeId: node.id,
|
|
16289
|
+
input,
|
|
16290
|
+
token,
|
|
16291
|
+
status,
|
|
16292
|
+
...description.note ? { note: description.note } : {}
|
|
16293
|
+
});
|
|
16294
|
+
}
|
|
16295
|
+
}
|
|
16296
|
+
}
|
|
16297
|
+
return findings;
|
|
16298
|
+
}
|
|
16299
|
+
|
|
16300
|
+
// src/commands/push.ts
|
|
16253
16301
|
function registerPush(program3) {
|
|
16254
16302
|
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
16303
|
const options = globalOptions(command);
|
|
@@ -16308,8 +16356,9 @@ function registerPush(program3) {
|
|
|
16308
16356
|
syncedHash: hash
|
|
16309
16357
|
});
|
|
16310
16358
|
const studioUrl = `${target.studioUrl}/workflow/${config.id}`;
|
|
16311
|
-
|
|
16359
|
+
emitWithVersionNote(
|
|
16312
16360
|
options,
|
|
16361
|
+
config,
|
|
16313
16362
|
`Pushed "${packed.title}": version ${result.version}, draft ${result.draft}` + (inlined.length ? ` (${inlined.length} file${inlined.length === 1 ? "" : "s"} inlined)` : "") + `.
|
|
16314
16363
|
${studioUrl}` + (unbacked.length ? `
|
|
16315
16364
|
Warning: ${unbacked.length} placeholder(s) not backed by test data:
|
|
@@ -16347,7 +16396,7 @@ function renderList() {
|
|
|
16347
16396
|
return [
|
|
16348
16397
|
table.toString(),
|
|
16349
16398
|
"This CLI offers only these node types on purpose: code-first workflows use custom code for everything else.",
|
|
16350
|
-
'Run "softr
|
|
16399
|
+
'Run "softr workflows spec <TYPE>" for the inputs, output and an example of one type.'
|
|
16351
16400
|
].join("\n");
|
|
16352
16401
|
}
|
|
16353
16402
|
function integrationIdFields(entry) {
|
|
@@ -16382,7 +16431,7 @@ ${table.toString()}`);
|
|
|
16382
16431
|
const idFields = integrationIdFields(entry);
|
|
16383
16432
|
if (idFields.length) {
|
|
16384
16433
|
blocks.push(
|
|
16385
|
-
`${idFields.join(", ")}: run "softr
|
|
16434
|
+
`${idFields.join(", ")}: run "softr integrations" for the ids of the workspace's connected integrations.`
|
|
16386
16435
|
);
|
|
16387
16436
|
}
|
|
16388
16437
|
} else {
|
|
@@ -16424,6 +16473,75 @@ function registerSpec(program3) {
|
|
|
16424
16473
|
});
|
|
16425
16474
|
}
|
|
16426
16475
|
|
|
16476
|
+
// src/commands/upgrade.ts
|
|
16477
|
+
function registerUpgrade(program3) {
|
|
16478
|
+
program3.command("upgrade").description(
|
|
16479
|
+
"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)"
|
|
16480
|
+
).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) => {
|
|
16481
|
+
const options = globalOptions(command);
|
|
16482
|
+
const dir = await findWorkflowDir(process.cwd(), opts.dir);
|
|
16483
|
+
const config = await readConfig(dir);
|
|
16484
|
+
const outdated = outdatedNodes(config);
|
|
16485
|
+
for (const id of nodeIds) {
|
|
16486
|
+
if (!locateNode(config, id)) {
|
|
16487
|
+
throw new CliError(
|
|
16488
|
+
`No node "${id}" in this workflow.`,
|
|
16489
|
+
'Node ids are the "id" values in softr-workflows.jsonc.'
|
|
16490
|
+
);
|
|
16491
|
+
}
|
|
16492
|
+
}
|
|
16493
|
+
const wanted = nodeIds.length ? outdated.filter((node) => nodeIds.includes(node.id)) : outdated;
|
|
16494
|
+
const current2 = nodeIds.filter((id) => !outdated.some((node) => node.id === id));
|
|
16495
|
+
const upgraded = [];
|
|
16496
|
+
const manual = [];
|
|
16497
|
+
const edits = [];
|
|
16498
|
+
for (const node of wanted) {
|
|
16499
|
+
const location = locateNode(config, node.id);
|
|
16500
|
+
const next = location ? upgradeOf(location.node) : void 0;
|
|
16501
|
+
if (!location || !next) {
|
|
16502
|
+
manual.push(node);
|
|
16503
|
+
continue;
|
|
16504
|
+
}
|
|
16505
|
+
edits.push({ path: location.path, value: next });
|
|
16506
|
+
upgraded.push(node);
|
|
16507
|
+
}
|
|
16508
|
+
if (edits.length) {
|
|
16509
|
+
await editConfig(dir, edits);
|
|
16510
|
+
}
|
|
16511
|
+
const headers = edits.length ? await refreshCodeHeaders(
|
|
16512
|
+
await readConfig(dir),
|
|
16513
|
+
dir,
|
|
16514
|
+
indexOutputs(await readStateFile(dir, OUTPUTS_FILE) ?? [])
|
|
16515
|
+
) : { refreshed: [], problems: [] };
|
|
16516
|
+
if (!wanted.length && manual.length === 0 && current2.length === 0) {
|
|
16517
|
+
emit(options, "Every node is on the current version of its type.", { upgraded: [], manual: [], current: [] });
|
|
16518
|
+
return;
|
|
16519
|
+
}
|
|
16520
|
+
if (headers.problems.length) {
|
|
16521
|
+
throw new CliError(
|
|
16522
|
+
`Upgraded, but a code file could not get its new header:
|
|
16523
|
+
${headers.problems.map((problem) => ` ${problem}`).join("\n")}`,
|
|
16524
|
+
'Fix the file, then run "pull" to regenerate its header.'
|
|
16525
|
+
);
|
|
16526
|
+
}
|
|
16527
|
+
const lines = [
|
|
16528
|
+
...upgraded.map((node) => `Upgraded ${node.id}: ${node.type} ${node.version} \u2192 ${node.current}.`),
|
|
16529
|
+
...manual.map(
|
|
16530
|
+
(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.`
|
|
16531
|
+
),
|
|
16532
|
+
...current2.map((id) => `${id} is already on the current version.`),
|
|
16533
|
+
headers.refreshed.length ? `Headers refreshed: ${headers.refreshed.join(", ")}` : "",
|
|
16534
|
+
upgraded.length ? "Then: softr workflows push, softr workflows test --all." : ""
|
|
16535
|
+
].filter(Boolean);
|
|
16536
|
+
emit(options, lines.join("\n"), {
|
|
16537
|
+
upgraded: upgraded.map((node) => ({ id: node.id, type: node.type, from: node.version, to: node.current })),
|
|
16538
|
+
manual: manual.map((node) => ({ id: node.id, type: node.type, from: node.version, to: node.current })),
|
|
16539
|
+
current: current2,
|
|
16540
|
+
headersRefreshed: headers.refreshed
|
|
16541
|
+
});
|
|
16542
|
+
});
|
|
16543
|
+
}
|
|
16544
|
+
|
|
16427
16545
|
// src/commands/verify.ts
|
|
16428
16546
|
function section(title, lines, okText) {
|
|
16429
16547
|
return { title, ok: lines.length === 0, lines: lines.length ? lines : [okText] };
|
|
@@ -16479,7 +16597,7 @@ function registerVerify(program3) {
|
|
|
16479
16597
|
`in sync with the server (version ${remote.version}, draft ${remote.draft})`
|
|
16480
16598
|
);
|
|
16481
16599
|
let rows = await getWorkflowOutputs(client, config.id);
|
|
16482
|
-
let assessment = await assess(client, config.id
|
|
16600
|
+
let assessment = await assess(client, config.id);
|
|
16483
16601
|
let ran = [];
|
|
16484
16602
|
if (opts.testMissing && sync.ok && files.ok) {
|
|
16485
16603
|
const wanted = new Set(
|
|
@@ -16489,23 +16607,19 @@ function registerVerify(program3) {
|
|
|
16489
16607
|
if (nodes.length) {
|
|
16490
16608
|
ran = await runNodeTests(client, config.id, nodes, {}, true);
|
|
16491
16609
|
rows = await getWorkflowOutputs(client, config.id);
|
|
16492
|
-
assessment = await assess(client, config.id
|
|
16610
|
+
assessment = await assess(client, config.id);
|
|
16493
16611
|
}
|
|
16494
16612
|
}
|
|
16495
16613
|
await writeStateFile(dir, OUTPUTS_FILE, mergeRows([], rows));
|
|
16496
16614
|
const headers = await refreshCodeHeaders(config, dir, indexOutputs(rows));
|
|
16497
|
-
if (assessment.checked
|
|
16615
|
+
if (assessment.checked.version !== remote.version || assessment.checked.draft !== remote.draft) {
|
|
16498
16616
|
syncLines.push(
|
|
16499
16617
|
`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
16618
|
);
|
|
16501
16619
|
}
|
|
16502
16620
|
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
16621
|
sections.push(
|
|
16622
|
+
section("Validation", assessment.validationProblems, "the service accepts the workflow for publishing"),
|
|
16509
16623
|
section(
|
|
16510
16624
|
"Tests",
|
|
16511
16625
|
assessment.testIssues.map((issue) => `${issue.id} (${issue.type}): ${issue.detail}`),
|
|
@@ -16525,17 +16639,15 @@ function registerVerify(program3) {
|
|
|
16525
16639
|
(item) => `${item.ok ? "OK " : "NOT"} ${item.title}
|
|
16526
16640
|
${item.lines.map((line) => ` ${line}`).join("\n")}`
|
|
16527
16641
|
),
|
|
16528
|
-
assessment.source === "client" ? CLIENT_SIDE_NOTE : "",
|
|
16529
16642
|
headers.refreshed.length ? `Headers refreshed: ${headers.refreshed.join(", ")}` : "",
|
|
16530
16643
|
ready ? "Ready to publish." : `Not ready to publish: ${sections.filter((item) => !item.ok).length} section(s) need attention.`
|
|
16531
16644
|
].filter(Boolean).join("\n\n");
|
|
16532
|
-
|
|
16645
|
+
emitWithVersionNote(options, config, human, {
|
|
16533
16646
|
workflowId: config.id,
|
|
16534
16647
|
ready,
|
|
16535
|
-
source: assessment.source,
|
|
16536
16648
|
files: { ok: files.ok, problems: fileProblems },
|
|
16537
16649
|
sync: { ok: sync.ok, problems: syncLines, server: { version: remote.version, draft: remote.draft } },
|
|
16538
|
-
validation:
|
|
16650
|
+
validation: { ok: assessment.validationProblems.length === 0, problems: assessment.validationProblems },
|
|
16539
16651
|
tests: {
|
|
16540
16652
|
ok: assessment.testIssues.length === 0,
|
|
16541
16653
|
issues: assessment.testIssues,
|
|
@@ -16549,10 +16661,36 @@ ${item.lines.map((line) => ` ${line}`).join("\n")}`
|
|
|
16549
16661
|
});
|
|
16550
16662
|
}
|
|
16551
16663
|
|
|
16664
|
+
// src/commands/workspaces.ts
|
|
16665
|
+
var import_cli_table36 = __toESM(require_cli_table3(), 1);
|
|
16666
|
+
function registerWorkspaces(program3) {
|
|
16667
|
+
program3.command("workspaces").description("The workspaces the stored token reaches, with your role in each").action(async (_opts, command) => {
|
|
16668
|
+
const options = globalOptions(command);
|
|
16669
|
+
const target = resolveTarget();
|
|
16670
|
+
const client = await authenticatedClient(target);
|
|
16671
|
+
const workspaces = await listWorkspaces(client);
|
|
16672
|
+
const table = new import_cli_table36.default({
|
|
16673
|
+
head: ["ID", "Name", "Role", "Default"],
|
|
16674
|
+
colWidths: [38, 34, 14, 9],
|
|
16675
|
+
wordWrap: true,
|
|
16676
|
+
style: { head: [], border: [], "padding-left": 1, "padding-right": 1 }
|
|
16677
|
+
});
|
|
16678
|
+
for (const workspace of workspaces) {
|
|
16679
|
+
table.push([workspace.id, workspace.name, workspace.userRole ?? "", workspace.isDefault ? "yes" : ""]);
|
|
16680
|
+
}
|
|
16681
|
+
emit(
|
|
16682
|
+
options,
|
|
16683
|
+
workspaces.length ? `${table.toString()}
|
|
16684
|
+
Pass --workspace <id> to a command, or set SOFTR_WORKSPACE_ID, to work in one of them.` : "The token reaches no workspace.",
|
|
16685
|
+
{ workspaces }
|
|
16686
|
+
);
|
|
16687
|
+
});
|
|
16688
|
+
}
|
|
16689
|
+
|
|
16552
16690
|
// package.json
|
|
16553
16691
|
var package_default = {
|
|
16554
16692
|
name: "@softr/cli",
|
|
16555
|
-
version: "0.
|
|
16693
|
+
version: "0.1.1-se21216.6",
|
|
16556
16694
|
description: "The Softr command line: build, test and publish Softr workflows from local files; more Softr APIs to come",
|
|
16557
16695
|
keywords: [
|
|
16558
16696
|
"softr",
|
|
@@ -16562,8 +16700,7 @@ var package_default = {
|
|
|
16562
16700
|
license: "UNLICENSED",
|
|
16563
16701
|
type: "module",
|
|
16564
16702
|
bin: {
|
|
16565
|
-
softr: "dist/cli.mjs"
|
|
16566
|
-
"softr-workflows": "dist/cli.mjs"
|
|
16703
|
+
softr: "dist/cli.mjs"
|
|
16567
16704
|
},
|
|
16568
16705
|
files: [
|
|
16569
16706
|
"dist"
|
|
@@ -16580,7 +16717,7 @@ var package_default = {
|
|
|
16580
16717
|
format: "prettier --write .",
|
|
16581
16718
|
"format:check": "prettier --check .",
|
|
16582
16719
|
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"
|
|
16720
|
+
"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
16721
|
},
|
|
16585
16722
|
publishConfig: {
|
|
16586
16723
|
access: "public"
|
|
@@ -16603,20 +16740,23 @@ var package_default = {
|
|
|
16603
16740
|
};
|
|
16604
16741
|
|
|
16605
16742
|
// src/cli.ts
|
|
16606
|
-
var program2 = new Command().name("softr").description("
|
|
16743
|
+
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
16744
|
registerLogin(program2);
|
|
16608
|
-
|
|
16745
|
+
registerWorkspaces(program2);
|
|
16609
16746
|
registerIntegrations(program2);
|
|
16610
|
-
|
|
16611
|
-
|
|
16612
|
-
|
|
16613
|
-
|
|
16614
|
-
|
|
16615
|
-
|
|
16616
|
-
|
|
16617
|
-
|
|
16618
|
-
|
|
16619
|
-
|
|
16620
|
-
|
|
16621
|
-
|
|
16747
|
+
var workflows = program2.command("workflows").description("Build, test and publish Softr Workflows from local files (one directory per workflow)");
|
|
16748
|
+
registerList(workflows);
|
|
16749
|
+
registerPull(workflows);
|
|
16750
|
+
registerPush(workflows);
|
|
16751
|
+
registerTest(workflows);
|
|
16752
|
+
registerVerify(workflows);
|
|
16753
|
+
registerOutputs(workflows);
|
|
16754
|
+
registerPlaceholder(workflows);
|
|
16755
|
+
registerInit(workflows);
|
|
16756
|
+
registerAdd(workflows);
|
|
16757
|
+
registerEdit(workflows);
|
|
16758
|
+
registerUpgrade(workflows);
|
|
16759
|
+
registerPublish(workflows);
|
|
16760
|
+
registerSpec(workflows);
|
|
16761
|
+
registerSchema(workflows);
|
|
16622
16762
|
program2.parseAsync(process.argv).catch(reportError);
|