@arbol-ai/cli 1.20261017.200040 → 1.20261018.212151

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 +9 -2
  2. package/dist/index.js +145 -21
  3. package/package.json +14 -4
package/README.md CHANGED
@@ -3,8 +3,9 @@
3
3
  `arbol`, the Arbol API from your terminal. Its commands are generated by
4
4
  [Cloudflare Forge](https://github.com/cloudflare/forge) from the platform's public OpenAPI,
5
5
  [`https://api.getarbol.com/openapi.json`](https://api.getarbol.com/openapi.json), and every request
6
- goes through [`@arbol-ai/sdk`](https://www.npmjs.com/package/@arbol-ai/sdk). The package's version is
7
- the API release its commands were generated from (`1.YYYYMMDD.n`).
6
+ goes through [`@arbol-ai/sdk`](https://www.npmjs.com/package/@arbol-ai/sdk). A release's version is
7
+ the date and time it was built (`1.YYYYMMDD.HHMMSS`, UTC); `arbol --version` also names the API release
8
+ its commands were generated from.
8
9
 
9
10
  ```sh
10
11
  npm install --global @arbol-ai/cli
@@ -31,6 +32,12 @@ ARBOL_PROFILE=ci arbol sura appointments list
31
32
 
32
33
  An API key given to one command, through `--api-key-stdin` or `ARBOL_API_KEY`, wins over every
33
34
  profile, as `GH_TOKEN` does for `gh`: scripts, servers and CI can run without saving anything.
35
+ When `arbol auth login` without `--profile` signs in as someone else, or to another organization,
36
+ than the default profile is, it asks where the login goes: a new profile (it suggests a name from the
37
+ organization), in place of the profile, or beside it in the same profile (`arbol auth switch` moves
38
+ between them). In a script it saves nothing and asks for `--profile` instead. `--profile <name>`
39
+ always saves the login in that profile.
40
+
34
41
  `arbol auth status` prints which credential is in use and, for a login, its organization;
35
42
  `arbol auth logout` signs a login out, or forgets a profile's API key (the key itself works until it
36
43
  is revoked in Arbol Account).
package/dist/index.js CHANGED
@@ -64673,6 +64673,27 @@ var schemaComponents = {
64673
64673
  }
64674
64674
  }
64675
64675
  };
64676
+ // src/generated/generation.json
64677
+ var generation_default = {
64678
+ formatVersion: 4,
64679
+ release: "1.20261017.200040",
64680
+ openapiSha256: "279d68eeef778edef60506856eff29b16919284d546ce9d9f92f6043c927b5a8",
64681
+ forge: {
64682
+ repository: "https://github.com/cloudflare/forge",
64683
+ commit: "7f86447210b7bea3779bfdab3cd5655eade0eadb",
64684
+ license: "Apache-2.0",
64685
+ version: "0.1.0"
64686
+ },
64687
+ plugins: {
64688
+ cli: "39007ea178b892e91916a2afa22883ba71b8f0a9c12b8a0db7f27d9fa128890d"
64689
+ },
64690
+ outputs: [
64691
+ {
64692
+ path: "src/generated/commands.ts",
64693
+ sha256: "6815a99764c8bf660482ba688da147eeae2ea2c1c82775964a5683acb5812dde"
64694
+ }
64695
+ ]
64696
+ };
64676
64697
 
64677
64698
  // src/oauth.ts
64678
64699
  import { spawn as spawn2 } from "node:child_process";
@@ -64707,14 +64728,8 @@ function openBrowser(url) {
64707
64728
  child.unref();
64708
64729
  }
64709
64730
  var ORGANIZATION = /^org_[A-Za-z0-9]+$/;
64710
- async function login(credentials, {
64711
- noBrowser = false,
64712
- organizationId,
64713
- nameOrganization = () => Promise.resolve(null)
64714
- } = {}) {
64715
- await credentials.initialize();
64716
- await credentials.store.get(credentials.loginKey("keyring-preflight", "any"));
64717
- const { config } = credentials;
64731
+ async function signIn(config, { noBrowser = false, organizationId, nameOrganization = () => Promise.resolve(null), preflight } = {}) {
64732
+ await preflight?.();
64718
64733
  const redirect = new URL(config.redirectUri);
64719
64734
  if (redirect.protocol !== "http:" || redirect.hostname !== "127.0.0.1" || !redirect.port) {
64720
64735
  throw new Error("The CLI can only receive a login on a 127.0.0.1 loopback address");
@@ -64798,17 +64813,52 @@ ${authorization.href}
64798
64813
  subject: claims.sub,
64799
64814
  organizationId: organization
64800
64815
  };
64801
- const name = await nameOrganization(tokens.access_token).catch(() => null);
64802
- await credentials.locked(credentials.loginKey(claims.sub, organization), () => credentials.save(session, name));
64803
- process.stderr.write(`Signed in to ${name ?? organization}${name ? ` (${organization})` : ""}.
64804
- `);
64805
- return { ...credentials.loginOf(session), organizationName: name };
64816
+ return { session, organizationName: await nameOrganization(tokens.access_token).catch(() => null) };
64806
64817
  } finally {
64807
64818
  clearTimeout(timeout);
64808
64819
  server.closeAllConnections();
64809
64820
  server.close();
64810
64821
  }
64811
64822
  }
64823
+ async function keep(credentials, { session, organizationName }) {
64824
+ await credentials.initialize();
64825
+ await credentials.locked(credentials.loginKey(session.subject, session.organizationId), () => credentials.save(session, organizationName));
64826
+ process.stderr.write(`Signed in to ${organizationName ?? session.organizationId}${organizationName ? ` (${session.organizationId})` : ""}.
64827
+ `);
64828
+ }
64829
+ async function login(credentials, options = {}) {
64830
+ await credentials.initialize();
64831
+ const signed = await signIn(credentials.config, {
64832
+ ...options,
64833
+ preflight: () => credentials.store.get(credentials.loginKey("keyring-preflight", "any"))
64834
+ });
64835
+ await keep(credentials, signed);
64836
+ }
64837
+ async function revoke(config, refreshToken) {
64838
+ try {
64839
+ const response = await oauth.revocationRequest(await authority(config), client(config), oauth.None(), refreshToken, {
64840
+ signal: AbortSignal.timeout(15000)
64841
+ });
64842
+ await response.body?.cancel();
64843
+ return response.ok;
64844
+ } catch {
64845
+ return false;
64846
+ }
64847
+ }
64848
+ async function forgetAll(credentials) {
64849
+ let confirmed = true;
64850
+ for (const login of await credentials.logins()) {
64851
+ await credentials.locked(login.key, async () => {
64852
+ const session = await credentials.session(login).catch(() => null);
64853
+ if (session?.refreshToken && !await revoke(credentials.config, session.refreshToken)) {
64854
+ confirmed = false;
64855
+ }
64856
+ await credentials.store.delete(login.key);
64857
+ await credentials.forget(login);
64858
+ });
64859
+ }
64860
+ return confirmed;
64861
+ }
64812
64862
  async function accessToken(credentials) {
64813
64863
  const login = await credentials.active();
64814
64864
  const again = `run arbol auth login --profile ${credentials.profile}`;
@@ -64995,9 +65045,10 @@ ${name}`);
64995
65045
  }
64996
65046
 
64997
65047
  // src/index.ts
64998
- var VERSION = "1.20261017.200040";
65048
+ var VERSION = "1.20261018.212151";
65049
+ var RELEASE = generation_default.release;
64999
65050
  var SERVICES = [...new Set(Object.keys(commands).map((name) => name.split(" ")[0]))].toSorted();
65000
- var HELP = `Arbol CLI ${VERSION}: the Arbol platform from your terminal.
65051
+ var HELP = `Arbol CLI ${VERSION} (Arbol API ${RELEASE}): the Arbol platform from your terminal.
65001
65052
 
65002
65053
  Usage
65003
65054
  arbol <command> [flags]
@@ -65194,7 +65245,7 @@ Run arbol --help.`);
65194
65245
  }
65195
65246
  const { positionals, values } = parsed;
65196
65247
  if (values.version) {
65197
- process.stdout.write(`arbol ${VERSION}
65248
+ process.stdout.write(`arbol ${VERSION} (Arbol API ${RELEASE})
65198
65249
  `);
65199
65250
  return;
65200
65251
  }
@@ -65294,12 +65345,39 @@ Run arbol --help.`);
65294
65345
  throw new UsageError(`Profile ${name} is for ${api}, not ${chosenApi()}; name another with --profile <name>`);
65295
65346
  }
65296
65347
  const auth = await discover(api);
65297
- await login(credentialsFor(name, auth), {
65348
+ const here = credentialsFor(name, auth);
65349
+ const signed = await signIn(auth, {
65298
65350
  noBrowser: values["no-browser"],
65299
- nameOrganization: (token) => nameOrganization(api, token)
65351
+ nameOrganization: (token) => nameOrganization(api, token),
65352
+ preflight: () => here.store.get(here.loginKey("keyring-preflight", "any"))
65300
65353
  });
65301
- await profiles.save(name, { kind: "login", auth });
65302
- process.stderr.write(`Saved${await saved(name)}.
65354
+ let destination = name;
65355
+ if (named === undefined && existing?.kind === "login") {
65356
+ const before = credentialsFor(name, existing.auth);
65357
+ const current = await before.active().catch(() => null);
65358
+ if (current && (current.subject !== signed.session.subject || current.organizationId !== signed.session.organizationId)) {
65359
+ const known = (await before.logins()).find((entry) => entry.key === current.key);
65360
+ let choice;
65361
+ try {
65362
+ choice = await placement(name, known ?? { ...current, organizationName: null }, signed, profiles);
65363
+ } catch (error) {
65364
+ if (signed.session.refreshToken) {
65365
+ await revoke(auth, signed.session.refreshToken);
65366
+ }
65367
+ throw error;
65368
+ }
65369
+ if (choice.kind === "new") {
65370
+ destination = choice.name;
65371
+ } else if (choice.kind === "replace" && !await forgetAll(before)) {
65372
+ process.stderr.write(`Profile ${name}'s previous login was removed, but its revocation could not be confirmed.
65373
+ `);
65374
+ }
65375
+ }
65376
+ }
65377
+ await keep(credentialsFor(destination, auth), signed);
65378
+ await profiles.save(destination, { kind: "login", auth });
65379
+ process.stderr.write(destination === name ? `Saved${await saved(name)}.
65380
+ ` : `Saved${await saved(destination)}; use it with --profile ${destination}, or by default after arbol auth use ${destination}.
65303
65381
  `);
65304
65382
  return;
65305
65383
  }
@@ -65603,6 +65681,51 @@ Type yes to continue: `);
65603
65681
  prompt.close();
65604
65682
  }
65605
65683
  }
65684
+ function suggestedName(organizationName, taken) {
65685
+ const base = folded(organizationName ?? "").replaceAll(/[^a-z0-9]+/g, "-").replaceAll(/^-+|-+$/g, "").slice(0, 28).replace(/-+$/, "") || "profile";
65686
+ let name = base;
65687
+ for (let next = 2;taken.includes(name); next++) {
65688
+ name = `${base}-${next}`;
65689
+ }
65690
+ return name;
65691
+ }
65692
+ async function placement(profile, current, signed, profiles) {
65693
+ const describe = (subject, organizationId, name) => `${name ? `${name} (${organizationId})` : organizationId} as ${subject}`;
65694
+ const now = describe(signed.session.subject, signed.session.organizationId, signed.organizationName);
65695
+ const before = describe(current.subject, current.organizationId, current.organizationName);
65696
+ if (!(process.stdin.isTTY && process.stderr.isTTY)) {
65697
+ throw new UsageError(`You signed in to ${now}, but profile ${profile} is ${before}. Nothing was saved: run arbol auth login --profile <name> to keep both, or arbol auth logout first to replace it.`);
65698
+ }
65699
+ const taken = (await profiles.list()).map(({ name }) => name);
65700
+ const suggestion = suggestedName(signed.organizationName, taken);
65701
+ process.stderr.write(`You signed in to ${now}.
65702
+ Profile ${profile} is ${before}.
65703
+ 1. Save it as a new profile (${suggestion})
65704
+ 2. Replace profile ${profile} with it
65705
+ 3. Add it to profile ${profile}; arbol auth switch moves between them
65706
+ `);
65707
+ const prompt = createInterface({ input: process.stdin, output: process.stderr });
65708
+ try {
65709
+ const answer = (await prompt.question("Choose 1, 2 or 3 [1]: ")).trim() || "1";
65710
+ if (answer === "2") {
65711
+ return { kind: "replace" };
65712
+ }
65713
+ if (answer === "3") {
65714
+ return { kind: "add" };
65715
+ }
65716
+ if (answer !== "1") {
65717
+ throw new UsageError("Choose 1, 2 or 3; nothing was saved.");
65718
+ }
65719
+ const name = (await prompt.question(`Profile name [${suggestion}]: `)).trim() || suggestion;
65720
+ validProfileName(name);
65721
+ if (taken.includes(name)) {
65722
+ throw new UsageError(`Profile ${name} exists; nothing was saved. Run arbol auth login --profile ${name} to sign it in.`);
65723
+ }
65724
+ return { kind: "new", name };
65725
+ } finally {
65726
+ prompt.close();
65727
+ }
65728
+ }
65606
65729
  async function nameOrganization(api, token) {
65607
65730
  const { data } = await createClient({ baseUrl: api, token }).account.organizations.current({});
65608
65731
  return typeof data.name === "string" ? data.name : null;
@@ -65742,5 +65865,6 @@ if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.ar
65742
65865
  }
65743
65866
  }
65744
65867
  export {
65745
- main
65868
+ main,
65869
+ suggestedName
65746
65870
  };
package/package.json CHANGED
@@ -1,8 +1,14 @@
1
1
  {
2
2
  "name": "@arbol-ai/cli",
3
- "version": "1.20261017.200040",
3
+ "version": "1.20261018.212151",
4
4
  "description": "The Arbol CLI: the Arbol API from your terminal, with commands generated by Cloudflare Forge from the public OpenAPI.",
5
- "keywords": ["arbol", "cli", "api", "openapi", "forge"],
5
+ "keywords": [
6
+ "arbol",
7
+ "cli",
8
+ "api",
9
+ "openapi",
10
+ "forge"
11
+ ],
6
12
  "homepage": "https://github.com/getarbol/project-arbol-cli#readme",
7
13
  "bugs": {
8
14
  "url": "https://github.com/getarbol/project-arbol-cli/issues"
@@ -15,7 +21,11 @@
15
21
  "bin": {
16
22
  "arbol": "./dist/index.js"
17
23
  },
18
- "files": ["dist", "README.md", "LICENSE"],
24
+ "files": [
25
+ "dist",
26
+ "README.md",
27
+ "LICENSE"
28
+ ],
19
29
  "type": "module",
20
30
  "engines": {
21
31
  "node": ">=26"
@@ -27,7 +37,7 @@
27
37
  "scripts": {
28
38
  "forge": "bun scripts/forge.ts",
29
39
  "openapi": "curl -fsS --proto =https --max-redirs 0 --max-filesize 33554432 -o openapi.json https://api.getarbol.com/openapi.json",
30
- "generate": "bun scripts/generate.ts openapi.json && bun pm pkg set version=\"$(jq -r .info.version openapi.json)\"",
40
+ "generate": "bun scripts/generate.ts openapi.json",
31
41
  "typecheck": "tsc --noEmit",
32
42
  "test": "bun test tests",
33
43
  "build": "bun scripts/build.ts",