@softr/cli 0.1.2-se21873.13 → 0.1.3
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 +10 -8
- package/dist/cli.mjs +91 -41
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -27,15 +27,17 @@ the environment, one login per environment). In a terminal, `login` asks which o
|
|
|
27
27
|
```bash
|
|
28
28
|
softr login # asks: 1. browser (recommended, OAuth + PKCE) or 2. paste a personal API token
|
|
29
29
|
softr login --no-browser # browser login, but prints the URL to open elsewhere; the CLI waits for the answer
|
|
30
|
-
softr login --token
|
|
30
|
+
softr login --token # a personal API token created in the studio (API tokens), typed in with the input hidden
|
|
31
|
+
pbpaste | softr login --token # the same token from stdin, for scripts
|
|
31
32
|
softr whoami # who the stored token belongs to and what it grants
|
|
32
33
|
softr logout
|
|
33
34
|
```
|
|
34
35
|
|
|
35
36
|
The browser login grants the scopes registered for the CLI in the studio, on every workspace you can reach; the consent
|
|
36
37
|
page lists them. The access token renews itself, and a login is needed again after 30 days without use. A personal API
|
|
37
|
-
token needs a `workflows:*` action in its statements; `login` warns when it has none. `
|
|
38
|
-
|
|
38
|
+
token needs a `workflows:*` action in its statements; `login` warns when it has none. `--token` takes no value: a token
|
|
39
|
+
on the command line ends up in the shell history, so `login` reads it from stdin when piped and asks for it otherwise.
|
|
40
|
+
`SOFTR_TOKEN` overrides the stored token for scripts and CI. The studio session cookie is not accepted.
|
|
39
41
|
|
|
40
42
|
The CLI targets Softr production. For development, `SOFTR_WORKFLOWS_API_URL`, `SOFTR_STUDIO_API_URL` and
|
|
41
43
|
`SOFTR_STUDIO_URL` point it at another workflows service, studio API and studio, for example locally running
|
|
@@ -43,11 +45,11 @@ ones. A workflow directory remembers the target it was pulled from.
|
|
|
43
45
|
|
|
44
46
|
## Global commands
|
|
45
47
|
|
|
46
|
-
| Command
|
|
47
|
-
|
|
|
48
|
-
| `login [--token
|
|
49
|
-
| `workspaces`
|
|
50
|
-
| `integrations [--workspace <id>] [--type <TYPE>]`
|
|
48
|
+
| Command | What it does |
|
|
49
|
+
| ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
50
|
+
| `login [--token] [--no-browser]` / `whoami` / `logout` | Log in through the browser (OAuth with PKCE), or store a personal API token (piped to stdin or typed in hidden) after confirming it with the studio; show the stored identity and its grants; forget the token. |
|
|
51
|
+
| `workspaces` | The workspaces the stored token reaches, with your role in each and the default one. Commands that take `--workspace` default to `SOFTR_WORKSPACE_ID` when it is set, else to your only or default workspace. |
|
|
52
|
+
| `integrations [--workspace <id>] [--type <TYPE>]` | List the workspace's connected integrations (id, name, type, account, auth kind) from the studio API; the id goes into a node's `integrationId` input. Inside a workflow directory the workspace is the workflow's; `--type` filters. Credentials are never shown. |
|
|
51
53
|
|
|
52
54
|
Every command accepts `--json`. `SOFTR_TOKEN` overrides the stored token; `SOFTR_WORKSPACE_ID` is the default workspace.
|
|
53
55
|
|
package/dist/cli.mjs
CHANGED
|
@@ -14471,6 +14471,9 @@ function authorizeUrl(target, redirectUri, state, challenge) {
|
|
|
14471
14471
|
}).toString();
|
|
14472
14472
|
return url.toString();
|
|
14473
14473
|
}
|
|
14474
|
+
function refreshFailureHint(status) {
|
|
14475
|
+
return status === 401 ? "The OAuth grant is gone (revoked or older than 30 days). Run: softr login" : "The studio could not renew the login right now; try again in a moment.";
|
|
14476
|
+
}
|
|
14474
14477
|
async function tokenRequest(target, form) {
|
|
14475
14478
|
const path13 = "/v1/oauth/token";
|
|
14476
14479
|
const response = await fetch(`${target.studioApiUrl}${path13}`, {
|
|
@@ -14484,7 +14487,7 @@ async function tokenRequest(target, form) {
|
|
|
14484
14487
|
`POST ${path13} failed with status ${response.status}${text ? `: ${text}` : ""}`,
|
|
14485
14488
|
response.status,
|
|
14486
14489
|
text,
|
|
14487
|
-
form["grant_type"] === "refresh_token" ?
|
|
14490
|
+
form["grant_type"] === "refresh_token" ? refreshFailureHint(response.status) : void 0
|
|
14488
14491
|
);
|
|
14489
14492
|
}
|
|
14490
14493
|
const tokens = await response.json();
|
|
@@ -14768,7 +14771,8 @@ var TITLE_WIDTH = 34;
|
|
|
14768
14771
|
function workflowTable(rows) {
|
|
14769
14772
|
const table = new import_cli_table3.default({
|
|
14770
14773
|
head: ["ID", "Title", "Triggers", "Status", "Version", "Draft"],
|
|
14771
|
-
|
|
14774
|
+
// Only the free-text title wraps; the other columns size to their content, so trigger types are never cut off.
|
|
14775
|
+
colWidths: [null, TITLE_WIDTH, null, null, null, null],
|
|
14772
14776
|
wordWrap: true,
|
|
14773
14777
|
style: { head: [], border: [], "padding-left": 1, "padding-right": 1 }
|
|
14774
14778
|
});
|
|
@@ -14776,7 +14780,7 @@ function workflowTable(rows) {
|
|
|
14776
14780
|
table.push([
|
|
14777
14781
|
row.id,
|
|
14778
14782
|
row.title,
|
|
14779
|
-
row.triggers.map((trigger) => trigger.type).join("\n"),
|
|
14783
|
+
row.triggers.map((trigger) => trigger.type).join("\n") || "-",
|
|
14780
14784
|
row.live === null ? "not enabled" : `live v${row.live}`,
|
|
14781
14785
|
row.version,
|
|
14782
14786
|
row.draft
|
|
@@ -15419,6 +15423,7 @@ ${renderIntegrations(rows)}`, {
|
|
|
15419
15423
|
import { execFile } from "node:child_process";
|
|
15420
15424
|
import { createServer } from "node:http";
|
|
15421
15425
|
import { stderr, stdin } from "node:process";
|
|
15426
|
+
import { emitKeypressEvents } from "node:readline";
|
|
15422
15427
|
import { createInterface } from "node:readline/promises";
|
|
15423
15428
|
var CALLBACK_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
15424
15429
|
async function identify(client) {
|
|
@@ -15435,10 +15440,11 @@ async function interactiveLogin(target) {
|
|
|
15435
15440
|
if (!stdin.isTTY) {
|
|
15436
15441
|
throw new CliError(
|
|
15437
15442
|
"No login method given and stdin is not a terminal.",
|
|
15438
|
-
`
|
|
15443
|
+
`Pipe a personal API token to "login --token", set ${TOKEN_ENV_VAR}, or run "login --no-browser" and open the printed URL.`
|
|
15439
15444
|
);
|
|
15440
15445
|
}
|
|
15441
15446
|
const rl = createInterface({ input: stdin, output: stderr });
|
|
15447
|
+
let answer = "";
|
|
15442
15448
|
try {
|
|
15443
15449
|
stderr.write(
|
|
15444
15450
|
`How do you want to log in to ${target.studioUrl}?
|
|
@@ -15446,7 +15452,6 @@ async function interactiveLogin(target) {
|
|
|
15446
15452
|
2. Personal API token: paste a pat_... created in the studio under API tokens
|
|
15447
15453
|
`
|
|
15448
15454
|
);
|
|
15449
|
-
let answer = "";
|
|
15450
15455
|
for (; ; ) {
|
|
15451
15456
|
answer = (await rl.question("Choose 1 or 2 [1]: ")).trim();
|
|
15452
15457
|
if (answer === "" || answer === "1" || answer === "2") {
|
|
@@ -15454,17 +15459,59 @@ async function interactiveLogin(target) {
|
|
|
15454
15459
|
}
|
|
15455
15460
|
stderr.write("Please answer 1 or 2.\n");
|
|
15456
15461
|
}
|
|
15457
|
-
if (answer === "2") {
|
|
15458
|
-
const token = (await rl.question("Personal API token: ")).trim();
|
|
15459
|
-
if (!token) {
|
|
15460
|
-
throw new CliError("No token given.", 'Run "softr login" again.');
|
|
15461
|
-
}
|
|
15462
|
-
return pastedToken(token);
|
|
15463
|
-
}
|
|
15464
15462
|
} finally {
|
|
15465
15463
|
rl.close();
|
|
15466
15464
|
}
|
|
15467
|
-
return browserLogin(target, false);
|
|
15465
|
+
return answer === "2" ? pastedToken(await promptToken()) : browserLogin(target, false);
|
|
15466
|
+
}
|
|
15467
|
+
function promptToken() {
|
|
15468
|
+
stderr.write("Personal API token: ");
|
|
15469
|
+
emitKeypressEvents(stdin);
|
|
15470
|
+
stdin.setRawMode(true);
|
|
15471
|
+
stdin.resume();
|
|
15472
|
+
return new Promise((resolve, reject) => {
|
|
15473
|
+
let token = "";
|
|
15474
|
+
const finish = (error) => {
|
|
15475
|
+
stdin.off("keypress", onKey);
|
|
15476
|
+
stdin.setRawMode(false);
|
|
15477
|
+
stdin.pause();
|
|
15478
|
+
stderr.write("\n");
|
|
15479
|
+
if (error) {
|
|
15480
|
+
reject(error);
|
|
15481
|
+
} else if (token.trim()) {
|
|
15482
|
+
resolve(token.trim());
|
|
15483
|
+
} else {
|
|
15484
|
+
reject(new CliError("No token given.", 'Run "softr login" again.'));
|
|
15485
|
+
}
|
|
15486
|
+
};
|
|
15487
|
+
const onKey = (text, key = {}) => {
|
|
15488
|
+
if (key.ctrl && (key.name === "c" || key.name === "d")) {
|
|
15489
|
+
finish(new CliError("Login cancelled."));
|
|
15490
|
+
} else if (key.name === "return" || key.name === "enter") {
|
|
15491
|
+
finish();
|
|
15492
|
+
} else if (key.name === "backspace") {
|
|
15493
|
+
if (token) {
|
|
15494
|
+
token = token.slice(0, -1);
|
|
15495
|
+
stderr.write("\b \b");
|
|
15496
|
+
}
|
|
15497
|
+
} else if (text && !key.ctrl && !key.meta && text >= " ") {
|
|
15498
|
+
token += text;
|
|
15499
|
+
stderr.write("*".repeat(text.length));
|
|
15500
|
+
}
|
|
15501
|
+
};
|
|
15502
|
+
stdin.on("keypress", onKey);
|
|
15503
|
+
});
|
|
15504
|
+
}
|
|
15505
|
+
async function readTokenFromStdin() {
|
|
15506
|
+
let input = "";
|
|
15507
|
+
for await (const chunk of stdin) {
|
|
15508
|
+
input += chunk;
|
|
15509
|
+
}
|
|
15510
|
+
const token = input.trim();
|
|
15511
|
+
if (!token) {
|
|
15512
|
+
throw new CliError("No token on stdin.", 'Pipe the token in, for example "pbpaste | softr login --token".');
|
|
15513
|
+
}
|
|
15514
|
+
return token;
|
|
15468
15515
|
}
|
|
15469
15516
|
function openBrowser(url) {
|
|
15470
15517
|
const [command, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["rundll32", ["url.dll,FileProtocolHandler", url]] : ["xdg-open", [url]];
|
|
@@ -15564,17 +15611,26 @@ function registerLogin(program3) {
|
|
|
15564
15611
|
program3.command("login").description(
|
|
15565
15612
|
"Log in through the browser (recommended) or with a personal API token; asks which when run in a terminal"
|
|
15566
15613
|
).option(
|
|
15567
|
-
"--token
|
|
15568
|
-
`a personal API token (pat_...) from the studio,
|
|
15569
|
-
).option("--no-browser", "browser login, but print the URL instead of opening it (for remote shells and agents)").action(async (opts, command) => {
|
|
15614
|
+
"--token",
|
|
15615
|
+
`log in with a personal API token (pat_...) from the studio: read from stdin when piped, else asked for with the input hidden; without the flag, $${TOKEN_ENV_VAR} is used when set`
|
|
15616
|
+
).option("--no-browser", "browser login, but print the URL instead of opening it (for remote shells and agents)").allowExcessArguments().allowUnknownOption().action(async (opts, command) => {
|
|
15570
15617
|
const options = globalOptions(command);
|
|
15571
15618
|
const target = resolveTarget();
|
|
15619
|
+
if (command.args.length > 0) {
|
|
15620
|
+
throw opts.token || command.args.some((arg) => arg.startsWith("--token=")) ? new CliError(
|
|
15621
|
+
'"--token" takes no value any more: a token on the command line ends up in the shell history.',
|
|
15622
|
+
'Pipe it in ("pbpaste | softr login --token") or run "softr login --token" and paste it at the prompt. Revoke the token you passed in the studio (API tokens) and create a new one.'
|
|
15623
|
+
) : new CliError(
|
|
15624
|
+
'"login" takes no arguments and knows only the options its help lists.',
|
|
15625
|
+
'Run "softr login --help".'
|
|
15626
|
+
);
|
|
15627
|
+
}
|
|
15572
15628
|
if (isProxyAuth()) {
|
|
15573
15629
|
throw new CliError(
|
|
15574
15630
|
`Login is not needed: the target authenticates the requests itself ($${AUTH_ENV_VAR}=proxy).`
|
|
15575
15631
|
);
|
|
15576
15632
|
}
|
|
15577
|
-
const token =
|
|
15633
|
+
const token = opts.token ? stdin.isTTY ? await promptToken() : await readTokenFromStdin() : (process.env[TOKEN_ENV_VAR] ?? "").trim();
|
|
15578
15634
|
const saved = token ? pastedToken(token) : opts.browser ? await interactiveLogin(target) : await browserLogin(target, true);
|
|
15579
15635
|
const identity = await identify({ target, token: saved.token, kind: saved.kind });
|
|
15580
15636
|
await report(options, target, saved, identity);
|
|
@@ -16085,6 +16141,18 @@ type: ${type}${example3} (${status})`, {
|
|
|
16085
16141
|
import { stdin as stdin2, stdout } from "node:process";
|
|
16086
16142
|
import { createInterface as createInterface2 } from "node:readline/promises";
|
|
16087
16143
|
|
|
16144
|
+
// src/project/open-workflow.ts
|
|
16145
|
+
async function openWorkflow(dir, missingIdHint = "Pull an existing workflow or create one with init first.") {
|
|
16146
|
+
const config = await readConfig(dir);
|
|
16147
|
+
if (!config.id) {
|
|
16148
|
+
throw new CliError('softr-workflows.jsonc has no "id".', missingIdHint);
|
|
16149
|
+
}
|
|
16150
|
+
const state = await readState(dir);
|
|
16151
|
+
const target = resolveTarget(state);
|
|
16152
|
+
const client = await authenticatedClient(target);
|
|
16153
|
+
return { config, id: config.id, state, target, client };
|
|
16154
|
+
}
|
|
16155
|
+
|
|
16088
16156
|
// src/project/readiness.ts
|
|
16089
16157
|
var REASONS = {
|
|
16090
16158
|
REFERENCED_NODE_UNKNOWN: "no node with that id",
|
|
@@ -16142,19 +16210,6 @@ async function assess(client, workflowId) {
|
|
|
16142
16210
|
}
|
|
16143
16211
|
|
|
16144
16212
|
// src/commands/publish.ts
|
|
16145
|
-
async function openWorkflow(dir) {
|
|
16146
|
-
const config = await readConfig(dir);
|
|
16147
|
-
if (!config.id) {
|
|
16148
|
-
throw new CliError(
|
|
16149
|
-
'softr-workflows.jsonc has no "id".',
|
|
16150
|
-
"Pull an existing workflow or create one with init first."
|
|
16151
|
-
);
|
|
16152
|
-
}
|
|
16153
|
-
const state = await readState(dir);
|
|
16154
|
-
const target = resolveTarget(state);
|
|
16155
|
-
const client = await authenticatedClient(target);
|
|
16156
|
-
return { config, id: config.id, state, target, client };
|
|
16157
|
-
}
|
|
16158
16213
|
function issuesOf(assessment, config) {
|
|
16159
16214
|
return [
|
|
16160
16215
|
...assessment.testIssues,
|
|
@@ -16284,16 +16339,11 @@ async function pinTarget(opts) {
|
|
|
16284
16339
|
if (opts.id) {
|
|
16285
16340
|
return { applicationId, workflowId: opts.id, client: await authenticatedClient(resolveTarget()) };
|
|
16286
16341
|
}
|
|
16287
|
-
const
|
|
16288
|
-
|
|
16289
|
-
|
|
16290
|
-
|
|
16291
|
-
|
|
16292
|
-
"Pass --id <workflowId>, or pull or init the workflow first."
|
|
16293
|
-
);
|
|
16294
|
-
}
|
|
16295
|
-
const target = resolveTarget(await readState(dir));
|
|
16296
|
-
return { applicationId, workflowId: config.id, client: await authenticatedClient(target) };
|
|
16342
|
+
const { id, client } = await openWorkflow(
|
|
16343
|
+
await findWorkflowDir(process.cwd(), opts.dir),
|
|
16344
|
+
"Pass --id <workflowId>, or pull or init the workflow first."
|
|
16345
|
+
);
|
|
16346
|
+
return { applicationId, workflowId: id, client };
|
|
16297
16347
|
}
|
|
16298
16348
|
function withPinOptions(command) {
|
|
16299
16349
|
return command.option("--app <applicationId>", "the application (default: $SOFTR_PINNED_APP_ID)").option("--id <workflowId>", "the workflow (default: the one in the workflow directory)").option("--dir <path>", "workflow directory (default: the current directory)");
|
|
@@ -16768,7 +16818,7 @@ Pass --workspace <id> to a command, or set SOFTR_WORKSPACE_ID, to work in one of
|
|
|
16768
16818
|
// package.json
|
|
16769
16819
|
var package_default = {
|
|
16770
16820
|
name: "@softr/cli",
|
|
16771
|
-
version: "0.1.
|
|
16821
|
+
version: "0.1.3",
|
|
16772
16822
|
description: "The Softr command line: build, test and publish Softr workflows from local files; more Softr APIs to come",
|
|
16773
16823
|
keywords: [
|
|
16774
16824
|
"softr",
|