@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.
Files changed (3) hide show
  1. package/README.md +10 -8
  2. package/dist/cli.mjs +91 -41
  3. 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 pat_... # a personal API token created in the studio (API tokens), no questions asked
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. `SOFTR_TOKEN` overrides the
38
- stored token for scripts and CI. The studio session cookie is not accepted.
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 | What it does |
47
- | ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
48
- | `login [--token <pat_...>] [--no-browser]` / `whoami` / `logout` | Log in through the browser (OAuth with PKCE), or store a personal API token after confirming it with the studio; show the stored identity and its grants; forget the token. |
49
- | `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. |
50
- | `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. |
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" ? "The OAuth grant is gone (revoked or older than 30 days). Run: softr login" : void 0
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
- colWidths: [38, TITLE_WIDTH, 30, 14, 9, 7],
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
- `Pass --token <pat_...>, set ${TOKEN_ENV_VAR}, or run "login --no-browser" and open the printed URL.`
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 <value>",
15568
- `a personal API token (pat_...) from the studio, no questions asked; defaults to $${TOKEN_ENV_VAR}`
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 = (opts.token ?? process.env[TOKEN_ENV_VAR] ?? "").trim();
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 dir = await findWorkflowDir(process.cwd(), opts.dir);
16288
- const config = await readConfig(dir);
16289
- if (!config.id) {
16290
- throw new CliError(
16291
- 'softr-workflows.jsonc has no "id".',
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.2-se21873.13",
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",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@softr/cli",
3
- "version": "0.1.2-se21873.13",
3
+ "version": "0.1.3",
4
4
  "description": "The Softr command line: build, test and publish Softr workflows from local files; more Softr APIs to come",
5
5
  "keywords": [
6
6
  "softr",