@arbol-ai/cli 1.20261017.200040 → 1.20261018.213455

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 +41 -3
  2. package/dist/index.js +145 -21
  3. package/package.json +14 -4
package/README.md CHANGED
@@ -3,15 +3,25 @@
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
11
12
  arbol --version
12
13
  ```
13
14
 
14
- It needs Node.js 26 or newer, whose `fetch` and `Temporal` the SDK uses.
15
+ It needs Node.js 26 or newer, whose `fetch` and `Temporal` the SDK uses. On macOS and Linux,
16
+ [cli.getarbol.com](https://cli.getarbol.com) does the same in one line:
17
+
18
+ ```sh
19
+ curl -fsSL https://cli.getarbol.com/install.sh | sh
20
+ ```
21
+
22
+ [`install.sh`](site/public/install.sh) checks for Node.js 26 and runs
23
+ `npm install --global @arbol-ai/cli`, or prints that command when npm is missing. Run it again to
24
+ update; `ARBOL_VERSION=<version>` installs a given release.
15
25
 
16
26
  ## Profiles
17
27
 
@@ -31,6 +41,12 @@ ARBOL_PROFILE=ci arbol sura appointments list
31
41
 
32
42
  An API key given to one command, through `--api-key-stdin` or `ARBOL_API_KEY`, wins over every
33
43
  profile, as `GH_TOKEN` does for `gh`: scripts, servers and CI can run without saving anything.
44
+ When `arbol auth login` without `--profile` signs in as someone else, or to another organization,
45
+ than the default profile is, it asks where the login goes: a new profile (it suggests a name from the
46
+ organization), in place of the profile, or beside it in the same profile (`arbol auth switch` moves
47
+ between them). In a script it saves nothing and asks for `--profile` instead. `--profile <name>`
48
+ always saves the login in that profile.
49
+
34
50
  `arbol auth status` prints which credential is in use and, for a login, its organization;
35
51
  `arbol auth logout` signs a login out, or forgets a profile's API key (the key itself works until it
36
52
  is revoked in Arbol Account).
@@ -138,6 +154,28 @@ bun run verify # typecheck, tests and dist/
138
154
  `openapi.json` is downloaded at generation time and not committed, and `src/generated/generation.json`
139
155
  records which release, Forge commit and plugin wrote each file.
140
156
 
157
+ ## Releases
158
+
159
+ A release of the API flows through three repositories, each started by the one before it:
160
+
161
+ 1. **project-arbol-gateway** publishes the OpenAPI at `https://api.getarbol.com/openapi.json` and sends
162
+ `openapi-release` to project-arbol-sdk-js.
163
+ 2. **project-arbol-sdk-js** regenerates `@arbol-ai/sdk` from it, publishes it to npm and sends
164
+ `sdk-release` (`{ "version": "<release>" }`) here.
165
+ 3. **Here**, [Generate](.github/workflows/generate.yml) pins `@arbol-ai/sdk` to exactly that release,
166
+ regenerates `src/generated/` from the live OpenAPI and, when anything changed and `bun run verify`
167
+ passes, commits `Release <release>` to main and runs [Publish](.github/workflows/publish.yml) on that
168
+ commit. When nothing changed, nothing is committed or published.
169
+
170
+ Generate also runs every 6 hours and by hand, pinning the SDK release the OpenAPI names once it is on
171
+ npm. Its push uses an installation token of the organization's GitHub App (Arbol Bot: the variable
172
+ `ARBOL_BOT_APP_ID` and the secret `ARBOL_BOT_PRIVATE_KEY`), so the commit runs Verify like any other.
173
+ Publish can also run by hand, for a change to the CLI alone; npm receives the package with the
174
+ organization's `NPM_TOKEN` and a provenance statement.
175
+
176
+ [cli.getarbol.com](https://cli.getarbol.com) is the Worker in [`site/`](site/wrangler.jsonc): static
177
+ files and no code. Cloudflare Workers Builds deploys it on every push to main that changes `site/`.
178
+
141
179
  ## License
142
180
 
143
181
  [Apache License 2.0](LICENSE)
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: "fcabf27a9d57a656f3e312e71160b76975ce3585173b8ae507cbabd236ba1cab"
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.213455";
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.213455",
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",