@deepdockai/cli 1.0.2 → 1.0.4

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/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.4
4
+
5
+ - `DEEPDOCK_CONFIG_DIR` moves the settings file (and only it) to a directory of your choice, on every platform; it must be an absolute path. Useful for keeping separate sign-ins apart, for example one per project or account.
6
+
7
+ ## 1.0.3
8
+
9
+ - `auth logout --keep-keys` revokes only the sign-in. On a terminal, plain `auth logout` lists the API keys and asks before revoking them (default: keep); with `--json`, `--yes` or no terminal it still revokes them. `keys create` says so.
10
+ - `keys create` no longer advises revoking the key "when you ship"; it prints the expiry and how to revoke.
11
+ - With exactly one organisation it is used automatically (and named), and `auth login` selects it. With several and none chosen, `org_required` stays.
12
+ - The approval page opens in your browser on a terminal (not in CI, without a display, with `--no-browser` or `DEEPDOCK_NO_BROWSER=1`). The line says "Opening the approval page in your browser" when it does.
13
+ - `Organisation:` is printed once per command, not again after the wait.
14
+ - Output colours by role: warning and error labels, success verbs, bold organisation and approval prompt, dim waiting line.
15
+ - The approval link is blue and underlined, and clickable in terminals that support links. Plain text when output is piped, under `--json`, or with `NO_COLOR`.
16
+
3
17
  ## 1.0.2
4
18
 
5
19
  - The install commands no longer pin a major version: `npx @deepdockai/cli auth login`, `npm install -g @deepdockai/cli`.
package/README.md CHANGED
@@ -30,14 +30,16 @@ holding only invented, marked data. While unpaired, the client shows a pairing c
30
30
 
31
31
  ## Quick start
32
32
 
33
- Sign in. The CLI prints a link; open it, check that the page names your computer and CLI version,
33
+ Sign in. The CLI prints the link and opens it in your browser (not in CI, without a display, with
34
+ `--no-browser` or `DEEPDOCK_NO_BROWSER=1`). Check that the page names your computer and CLI version,
34
35
  and click Approve. No password is typed into the terminal.
35
36
 
36
37
  ```
37
38
  $ deepdock auth login
38
- Open this link to approve the sign-in (it expires in 10 minutes):
39
+ Opening the approval page in your browser (it expires in 10 minutes):
39
40
  https://dash.deepdock.ai/cli?request=rq_...
40
41
  The page shows this computer's name and the CLI version: check them before you approve.
42
+ Opened in your browser.
41
43
  Waiting for approval in the browser.
42
44
 
43
45
  Signed in as anna.mueller@muster-it.example.
@@ -91,7 +93,7 @@ deepdock skill
91
93
  | ----------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
92
94
  | `auth login` | sign in as yourself, approved in the browser; the sign-in lasts 90 days |
93
95
  | `auth status` | who is signed in, the organisations, and the keys this CLI created (id, name, prefix, expiry, file; never the value); exits 5 when not signed in, like `gh auth status` |
94
- | `auth logout` | revoke the sign-in and every key this CLI created |
96
+ | `auth logout [--keep-keys]` | revoke the sign-in and every key this CLI created (`--keep-keys`: only the sign-in) |
95
97
  | `org list`, `org select [slug]` | your organisations; set the default one |
96
98
  | `systems list` | the systems and actions assigned to your organisation |
97
99
  | `systems openapi <system>` | a system's OpenAPI document on stdout, bare when redirected; save it to a file |
@@ -110,7 +112,8 @@ and `keys revoke` without a question, `-h, --help` everywhere and `-v, --version
110
112
 
111
113
  Two things are never guessed. A device is always named by its full id: no default device, no
112
114
  lookup by label. The organisation comes from `--org`, then `DEEPDOCK_ORG`, then the default
113
- chosen with `deepdock org select`; with none the command stops and lists your organisations.
115
+ chosen with `deepdock org select`. With exactly one organisation it is used without asking (and
116
+ named on stderr); with several and none chosen the command stops and lists them.
114
117
 
115
118
  ## The approval flow
116
119
 
@@ -222,7 +225,8 @@ macOS and Linux need nothing special. On Windows:
222
225
  - **Keys expire and can be revoked.** A key reaches every device of the organisation until it
223
226
  expires (at most 90 days from the CLI) or is revoked. `deepdock keys revoke <id>` revokes it and
224
227
  removes the line the CLI wrote; `deepdock auth logout` revokes the sign-in and every key this
225
- CLI created. In `--json`, `data.removed` says what happened to the line: `null` for a key this
228
+ CLI created, unless you pass `--keep-keys`. On a terminal it lists the keys and asks first
229
+ (default: keep); with `--json` or without a terminal it revokes them unless `--keep-keys` is passed. In `--json`, `data.removed` says what happened to the line: `null` for a key this
226
230
  CLI never wrote, otherwise `{ file, name, removal }` with `removal` `done`, `absent` (the line
227
231
  was already gone or holds another value) or `failed` (with a `reason`). If the file is held open
228
232
  by another program (a dev server watching it, on Windows), the key is still revoked and the
@@ -232,12 +236,13 @@ macOS and Linux need nothing special. On Windows:
232
236
  revoked, and `data.alreadyRevoked` says so). `auth logout` reports the failure in
233
237
  `apiKeysRevoked[].removed` and in `meta.hint`, and clears its local records either way.
234
238
  - **Local state** lives in `$XDG_CONFIG_HOME/deepdock/config.json`, else
235
- `~/.config/deepdock/config.json` (`%APPDATA%\deepdock\config.json` on Windows), mode 600 in a
236
- mode 700 directory. It holds the sign-in, the default organisation, the ids of the keys this
239
+ `~/.config/deepdock/config.json` (`%APPDATA%\deepdock\config.json` on Windows), or in
240
+ `$DEEPDOCK_CONFIG_DIR/config.json` when that absolute path is set; mode 600, in a directory the
241
+ CLI creates mode 700 when it does not exist. It holds the sign-in, the default organisation, the ids of the keys this
237
242
  CLI created (never their values) and a pending approval request. A file other users can read
238
243
  is refused with the `chmod` to run.
239
244
  - **Environment:** `DEEPDOCK_ORG`, `DEEPDOCK_BASE_URL` (https, or http for localhost only),
240
- `NO_COLOR`, `FORCE_COLOR` (colours messages on a stream that is not a terminal; `NO_COLOR` wins). A sign-in or pending request is never sent to a different base URL than the one it
245
+ `DEEPDOCK_CONFIG_DIR`, `NO_COLOR`, `FORCE_COLOR` (colours messages on a stream that is not a terminal; `NO_COLOR` wins). A sign-in or pending request is never sent to a different base URL than the one it
241
246
  was made against.
242
247
 
243
248
  Report a vulnerability by email to torben@deepdock.ai, not in a public issue: [SECURITY.md](SECURITY.md).
@@ -252,11 +257,33 @@ bun run test:node # the same tests under Node (needs dist/ from the build)
252
257
  bun run format && bun run lint && bun run typecheck && bun run check:public
253
258
  ```
254
259
 
255
- `tests/fakeApi.ts` is a local HTTP server answering the routes the CLI uses:
260
+ ### Trying a change locally
261
+
262
+ `bun run dev` runs the CLI from source. Each API gets a settings directory of its own under
263
+ `<config dir>/deepdock-dev/<api host>/`, so a sign-in made against one API never sits next to
264
+ another's; only the CLI's own settings move (`DEEPDOCK_CONFIG_DIR`), nothing else on the machine:
265
+
266
+ ```bash
267
+ bun run dev -- auth status # the production API
268
+ DEEPDOCK_BASE_URL=http://localhost:8787 bun run dev -- auth login --no-wait
269
+ ```
270
+
271
+ The script does not depend on the current directory: from elsewhere, call it by its path
272
+ (`bun /path/to/devkit/scripts/dev.ts ...`), or put the `DEEPDOCK_BASE_URL=... bun .../dev.ts` line
273
+ in a shell alias per API. To run the exact tarball npm would publish, from any directory:
274
+
275
+ ```bash
276
+ bun run pack:try -- auth status # builds, packs, prints the one-line command to run it
277
+ ```
278
+
279
+ It prints `DEEPDOCK_BASE_URL=... DEEPDOCK_CONFIG_DIR=... npx -y --package=/path/to/deepdockai-cli-<version>.tgz deepdock auth status`,
280
+ for the same API and settings directory `bun run dev` would use (`npx <tarball>` alone would try
281
+ to execute the archive). `tests/fakeApi.ts` is a local stand-in that approves every request at
282
+ once:
256
283
 
257
284
  ```bash
258
285
  bun scripts/fakeServer.ts & # prints its URL
259
- DEEPDOCK_BASE_URL=http://127.0.0.1:<port> node dist/deepdock.js auth login
286
+ DEEPDOCK_BASE_URL=http://127.0.0.1:<port> bun run dev -- auth login
260
287
  ```
261
288
 
262
289
  ## Licence
package/dist/deepdock.js CHANGED
@@ -11323,6 +11323,18 @@ var parseAnswer = async (name, value) => {
11323
11323
  return result;
11324
11324
  };
11325
11325
 
11326
+ // src/approval/browser.ts
11327
+ var isSet = (value) => value !== undefined && !["", "0", "false"].includes(value.toLowerCase());
11328
+ var browserAllowed = (rt, noBrowser) => {
11329
+ if (noBrowser || isSet(rt.env.DEEPDOCK_NO_BROWSER) || isSet(rt.env.CI))
11330
+ return false;
11331
+ if (isSet(rt.env.SSH_CONNECTION) && rt.platform !== "linux")
11332
+ return false;
11333
+ if (rt.platform === "linux" && !rt.env.DISPLAY && !rt.env.WAYLAND_DISPLAY)
11334
+ return false;
11335
+ return true;
11336
+ };
11337
+
11326
11338
  // src/approval/poll.ts
11327
11339
  var MAX_CONSECUTIVE_FAILURES = 5;
11328
11340
  var EXPIRY_GRACE_MS = 15000;
@@ -11412,7 +11424,7 @@ import { spawn } from "node:child_process";
11412
11424
  import { homedir, hostname } from "node:os";
11413
11425
 
11414
11426
  // src/version.ts
11415
- var VERSION = "1.0.2";
11427
+ var VERSION = "1.0.4";
11416
11428
 
11417
11429
  // src/runtime.ts
11418
11430
  var DEFAULT_BASE_URL = "https://api.deepdock.ai";
@@ -11423,16 +11435,19 @@ var browserCommand = (platform, url) => {
11423
11435
  return { command: "rundll32", args: ["url.dll,FileProtocolHandler", url] };
11424
11436
  return { command: "xdg-open", args: [url] };
11425
11437
  };
11426
- var openInBrowser = (url) => {
11438
+ var openInBrowser = (url) => new Promise((resolve) => {
11427
11439
  const { command, args } = browserCommand(process.platform, url);
11428
11440
  try {
11429
11441
  const child = spawn(command, args, { stdio: "ignore", detached: true });
11430
- child.on("error", () => {
11431
- return;
11442
+ child.once("error", () => resolve(false));
11443
+ child.once("spawn", () => {
11444
+ child.unref();
11445
+ resolve(true);
11432
11446
  });
11433
- child.unref();
11434
- } catch {}
11435
- };
11447
+ } catch {
11448
+ resolve(false);
11449
+ }
11450
+ });
11436
11451
  var defaultRuntime = () => {
11437
11452
  const interrupt = new AbortController;
11438
11453
  process.once("SIGINT", () => interrupt.abort());
@@ -11638,6 +11653,9 @@ var requestedSlug = (flag, env, config) => flag || env.DEEPDOCK_ORG || config.de
11638
11653
  var resolveOrg = (requested, orgs, fromStoredDefault = false) => {
11639
11654
  const choices = selectableOrgs(orgs);
11640
11655
  const slugs = choices.map((org) => org.slug);
11656
+ const [only] = choices;
11657
+ if (!requested && only && choices.length === 1)
11658
+ return { ...only, onlyOne: true };
11641
11659
  if (!requested) {
11642
11660
  throw new CliError({
11643
11661
  code: "org_required",
@@ -11696,17 +11714,21 @@ var renameWithRetry = async (from, to) => {
11696
11714
  // src/config/store.ts
11697
11715
  var PENDING_KEPT_MS = 60 * 60 * 1000;
11698
11716
  var emptyConfig = () => ({ keys: [], pending: [] });
11699
- var configPath = (rt) => {
11700
- if (rt.platform === "win32") {
11701
- const base = rt.env.APPDATA ?? join(rt.homedir, "AppData", "Roaming");
11702
- return join(base, "deepdock", "config.json");
11717
+ var absoluteDir = (env, name) => {
11718
+ const value = env[name];
11719
+ if (value && !isAbsolute(value)) {
11720
+ throw usageError(`${name} must be an absolute path.`, "Unset it or give a full path.");
11703
11721
  }
11704
- const xdg = rt.env.XDG_CONFIG_HOME;
11705
- if (xdg && !isAbsolute(xdg)) {
11706
- throw usageError("XDG_CONFIG_HOME must be an absolute path.", "Unset it or give a full path.");
11707
- }
11708
- const base = xdg || join(rt.homedir, ".config");
11709
- return join(base, "deepdock", "config.json");
11722
+ return value || undefined;
11723
+ };
11724
+ var configBaseDir = (rt) => {
11725
+ if (rt.platform === "win32")
11726
+ return rt.env.APPDATA || join(rt.homedir, "AppData", "Roaming");
11727
+ return absoluteDir(rt.env, "XDG_CONFIG_HOME") ?? join(rt.homedir, ".config");
11728
+ };
11729
+ var configPath = (rt) => {
11730
+ const own = absoluteDir(rt.env, "DEEPDOCK_CONFIG_DIR");
11731
+ return own ? join(own, "config.json") : join(configBaseDir(rt), "deepdock", "config.json");
11710
11732
  };
11711
11733
  var isRecord3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
11712
11734
  var text = (value) => typeof value === "string" ? value : undefined;
@@ -11747,7 +11769,16 @@ var pickKey = (value) => {
11747
11769
  const { org, id, name, prefix, expiresAt, file, variable } = entry;
11748
11770
  if (!org || !id || !name || !prefix || !expiresAt || !file || !variable)
11749
11771
  return;
11750
- return { org, id, name, prefix, expiresAt, file, variable };
11772
+ return {
11773
+ org,
11774
+ id,
11775
+ name,
11776
+ prefix,
11777
+ expiresAt,
11778
+ file,
11779
+ variable,
11780
+ ...value.kept === true ? { kept: true } : {}
11781
+ };
11751
11782
  };
11752
11783
  var pickPending = (value) => {
11753
11784
  if (!isRecord3(value))
@@ -11900,11 +11931,29 @@ var updateConfig = (rt, change) => withLock(rt, async () => {
11900
11931
 
11901
11932
  // src/output/styles.ts
11902
11933
  import { styleText } from "node:util";
11934
+ var OSC = "\x1B]";
11935
+ var ST = "\x1B\\";
11936
+ var ROLES = {
11937
+ warn: "yellow",
11938
+ error: "red",
11939
+ success: "green",
11940
+ emphasis: "bold",
11941
+ quiet: "dim",
11942
+ link: "link"
11943
+ };
11903
11944
  var makeStyler = (rt, stream) => {
11904
11945
  const forced = rt.env.FORCE_COLOR !== undefined && !["", "0"].includes(rt.env.FORCE_COLOR);
11905
11946
  const enabled = (stream.isTTY || forced) && !rt.env.NO_COLOR && rt.env.TERM !== "dumb";
11906
- return (name, text) => enabled ? styleText(name, text, { validateStream: false }) : text;
11947
+ return (name, text) => {
11948
+ if (!enabled)
11949
+ return text;
11950
+ if (name !== "link")
11951
+ return styleText(name, text, { validateStream: false });
11952
+ const styled = styleText(["blue", "underline"], text, { validateStream: false });
11953
+ return `${OSC}8;;${text}${ST}${styled}${OSC}8;;${ST}`;
11954
+ };
11907
11955
  };
11956
+ var paint = (style, role, text) => style(ROLES[role], text);
11908
11957
 
11909
11958
  // src/output/terminal.ts
11910
11959
  var isInteractive = (rt, json) => rt.stdin.isTTY && rt.stdout.isTTY && !json;
@@ -11926,6 +11975,7 @@ var createContext = (rt, opts) => {
11926
11975
  const baseUrl = baseUrlFor(rt);
11927
11976
  const api = createApi(rt);
11928
11977
  let loaded;
11978
+ let announcedOrg = false;
11929
11979
  const config = async () => {
11930
11980
  loaded ??= await loadConfig(rt);
11931
11981
  return loaded;
@@ -11946,7 +11996,8 @@ var createContext = (rt, opts) => {
11946
11996
  return loaded;
11947
11997
  },
11948
11998
  note: (line) => {
11949
- rt.stderr.write(`${line}
11999
+ const warning = "Warning:";
12000
+ rt.stderr.write(`${line.startsWith(warning) ? `${paint(ctx.err, "warn", warning)}${line.slice(warning.length)}` : line}
11950
12001
  `);
11951
12002
  },
11952
12003
  signInSecret: async () => {
@@ -11978,7 +12029,10 @@ var createContext = (rt, opts) => {
11978
12029
  }
11979
12030
  const resolved = resolveOrg(requested, orgs, fromStoredDefault);
11980
12031
  ctx.org = { slug: resolved.slug, name: resolved.name };
11981
- ctx.note(ctx.err("dim", `Organisation: ${resolved.name} (${resolved.slug})`));
12032
+ if (!announcedOrg) {
12033
+ announcedOrg = true;
12034
+ ctx.note(resolved.onlyOne ? `${paint(ctx.err, "quiet", "Using organisation")} ${paint(ctx.err, "emphasis", resolved.name)} ${paint(ctx.err, "quiet", `(${resolved.slug}).`)}` : `${paint(ctx.err, "quiet", "Organisation:")} ${paint(ctx.err, "emphasis", resolved.name)} ${paint(ctx.err, "quiet", `(${resolved.slug})`)}`);
12035
+ }
11982
12036
  return resolved;
11983
12037
  },
11984
12038
  callInOrg: async (req) => {
@@ -12029,7 +12083,7 @@ Examples
12029
12083
 
12030
12084
  More
12031
12085
  Exit codes: \`deepdock help exit-codes\`. API reference per system: \`deepdock systems openapi <system>\`.
12032
- Environment: DEEPDOCK_ORG, DEEPDOCK_BASE_URL, NO_COLOR, FORCE_COLOR.
12086
+ Environment: DEEPDOCK_ORG, DEEPDOCK_BASE_URL, DEEPDOCK_CONFIG_DIR, NO_COLOR, FORCE_COLOR.
12033
12087
  Actions run with an API key from your own code, never from this CLI: \`deepdock keys create --help\`.
12034
12088
  `;
12035
12089
  var AUTH_LOGIN_HELP = `Sign in to Deepdock as yourself.
@@ -12044,12 +12098,14 @@ an action; actions run from your code with an API key (\`deepdock keys create\`)
12044
12098
  Usage
12045
12099
  deepdock auth login [flags]
12046
12100
 
12047
- The link is printed either way; opening the browser is a convenience that may fail silently.
12101
+ The link is printed either way. On a terminal the CLI also opens it in your browser, unless
12102
+ CI is set, there is no display, DEEPDOCK_NO_BROWSER=1 is set or you pass --no-browser.
12048
12103
 
12049
12104
  Flags
12050
12105
  --no-wait Print the request and exit; finish it later with --wait (the default
12051
12106
  when there is no terminal)
12052
12107
  --wait <request> Finish a request started with --no-wait
12108
+ --no-browser Do not open the link in a browser
12053
12109
 
12054
12110
  Examples
12055
12111
  $ deepdock auth login
@@ -12057,7 +12113,7 @@ Examples
12057
12113
  $ deepdock auth login --wait <request>
12058
12114
 
12059
12115
  More
12060
- \`deepdock auth status\` shows who is signed in (exit 5 when nobody is). \`deepdock auth logout\` revokes the sign-in.
12116
+ \`deepdock auth status\` shows who is signed in (exit 5 when nobody is). \`deepdock auth logout\` revokes the sign-in and, unless you pass --keep-keys, the API keys this CLI created.
12061
12117
  `;
12062
12118
  var KEYS_CREATE_HELP = `Create an API key for your organisation (admin).
12063
12119
 
@@ -12066,7 +12122,8 @@ key once, writes it into the env file named by --write-env, and keeps no copy. T
12066
12122
  never printed and the CLI never uses it; your code reads it from that file.
12067
12123
 
12068
12124
  A key reaches every device of the organisation until it expires or is revoked. Keep the
12069
- expiry short while you build, and revoke it when you ship: \`deepdock keys revoke <id>\`.
12125
+ expiry short, and revoke a key you no longer need: \`deepdock keys revoke <id>\`. \`deepdock auth logout\`
12126
+ revokes the keys this CLI created unless you pass --keep-keys.
12070
12127
 
12071
12128
  Usage
12072
12129
  deepdock keys create <name> --expires <duration> --write-env <file> [flags]
@@ -12081,6 +12138,7 @@ Flags
12081
12138
  the CLI warns
12082
12139
  --no-wait Print the request and exit; finish it with --wait
12083
12140
  --wait <request> Finish a request started with --no-wait
12141
+ --no-browser Do not open the link in a browser (also DEEPDOCK_NO_BROWSER=1)
12084
12142
 
12085
12143
  Examples
12086
12144
  $ deepdock keys create "myapp dev" --expires 30d --write-env .env.local
@@ -12153,6 +12211,34 @@ var formatHelp = (command) => {
12153
12211
  `;
12154
12212
  };
12155
12213
 
12214
+ // src/output/confirm.ts
12215
+ import { createInterface } from "node:readline";
12216
+ var ask = (rt, prompt) => new Promise((resolve) => {
12217
+ const rl = createInterface({ input: rt.stdin.stream, output: undefined, terminal: false });
12218
+ rt.stderr.write(prompt);
12219
+ let settled = false;
12220
+ rl.once("line", (line) => {
12221
+ settled = true;
12222
+ rl.close();
12223
+ resolve(line.trim());
12224
+ });
12225
+ rl.once("close", () => {
12226
+ if (!settled)
12227
+ resolve("");
12228
+ });
12229
+ });
12230
+ var confirm = async (rt, question) => {
12231
+ const answer = await ask(rt, `${question} [y/N]: `);
12232
+ return /^y(es)?$/i.test(answer);
12233
+ };
12234
+ var pickIndex = async (rt, count, defaultIndex) => {
12235
+ const answer = await ask(rt, `Number [${defaultIndex}]: `);
12236
+ const chosen = answer === "" ? defaultIndex : Number.parseInt(answer, 10);
12237
+ if (!Number.isInteger(chosen) || chosen < 1 || chosen > count)
12238
+ return;
12239
+ return chosen;
12240
+ };
12241
+
12156
12242
  // src/writeEnv/ledger.ts
12157
12243
  import { relative, sep } from "node:path";
12158
12244
  var addKey = (config, entry) => {
@@ -12393,11 +12479,19 @@ var settleWrittenLine = async (ctx, entry, keepEntryOnFailure) => {
12393
12479
 
12394
12480
  // src/commands/auth.ts
12395
12481
  var minutesLeft = (ctx, expiresAt) => Math.max(1, Math.ceil((Date.parse(expiresAt) - ctx.rt.now().getTime()) / 60000));
12396
- var announceApproval = (ctx, what, url, expiresAt, extra) => {
12397
- ctx.note(`Open this link to approve ${what} (it expires in ${minutesLeft(ctx, expiresAt)} minutes):`);
12398
- ctx.note(` ${url}`);
12399
- ctx.note(extra ?? "The page shows this computer's name and the CLI version: check them before you approve.");
12400
- ctx.note("Waiting for approval in the browser.");
12482
+ var noteWaiting = (ctx) => {
12483
+ ctx.note(paint(ctx.err, "quiet", "Waiting for approval in the browser."));
12484
+ };
12485
+ var announceApproval = async (ctx, what, url, expiresAt, options) => {
12486
+ const open = browserAllowed(ctx.rt, options.noBrowser);
12487
+ const minutes = minutesLeft(ctx, expiresAt);
12488
+ ctx.note(open ? `Opening the approval page in your browser (it expires in ${minutes} minutes):` : `Open this link to approve ${what} (it expires in ${minutes} minutes):`);
12489
+ ctx.note(` ${ctx.err("link", url)}`);
12490
+ ctx.note(options.extra ?? "The page shows this computer's name and the CLI version: check them before you approve.");
12491
+ if (open) {
12492
+ ctx.note(await ctx.rt.openBrowser(url) ? "Opened in your browser." : "Could not open a browser; use the link above.");
12493
+ }
12494
+ noteWaiting(ctx);
12401
12495
  };
12402
12496
  var pendingResult = (pending, url, text) => ({
12403
12497
  data: { pending: { request: pending.request, url, expiresAt: pending.expiresAt } },
@@ -12441,14 +12535,22 @@ var finishLogin = async (ctx, pending) => {
12441
12535
  current.organisations = answer.organisations;
12442
12536
  if (current.defaultOrg && !slugs.includes(current.defaultOrg))
12443
12537
  current.defaultOrg = undefined;
12538
+ if (!current.defaultOrg && slugs.length === 1)
12539
+ current.defaultOrg = slugs[0];
12444
12540
  current.pending = current.pending.filter((entry) => entry.request !== pending.request);
12445
12541
  });
12446
- const lines = [`Signed in as ${answer.user.email}.`, ""];
12542
+ const lines = [`${paint(ctx.out, "success", "Signed in")} as ${answer.user.email}.`, ""];
12447
12543
  if (answer.organisations.length === 0) {
12448
12544
  lines.push("You are not a member of an organisation yet.");
12449
12545
  } else {
12450
12546
  lines.push("Your organisations:", indented(orgTable(answer.organisations, config.defaultOrg, ctx.out)), "");
12451
- lines.push("Choose one with `deepdock org select <slug>` or pass --org to each command.");
12547
+ const only = selectableOrgs(answer.organisations);
12548
+ const [sole] = only;
12549
+ if (sole && only.length === 1 && config.defaultOrg === sole.slug) {
12550
+ lines.push(`${paint(ctx.out, "success", "Selected")} ${sole.name} (${sole.slug}) as your organisation.`);
12551
+ } else {
12552
+ lines.push("Choose one with `deepdock org select <slug>` or pass --org to each command.");
12553
+ }
12452
12554
  }
12453
12555
  return {
12454
12556
  data: {
@@ -12471,7 +12573,7 @@ var login = async (ctx, options) => {
12471
12573
  }
12472
12574
  assertSameBaseUrl(pending.baseUrl, ctx.baseUrl, "request");
12473
12575
  assertNotExpired(ctx.rt, "login", pending.expiresAt);
12474
- ctx.note("Waiting for approval in the browser.");
12576
+ noteWaiting(ctx);
12475
12577
  return finishLogin(ctx, pending);
12476
12578
  }
12477
12579
  const started = await startLogin(ctx.api, ctx.rt);
@@ -12485,13 +12587,14 @@ var login = async (ctx, options) => {
12485
12587
  await upsertPending(ctx, pending);
12486
12588
  if (options.wait === false || !ctx.interactive) {
12487
12589
  return pendingResult(pending, started.url, [
12488
- `Open this link to approve the sign-in: ${started.url}`,
12590
+ `Open this link to approve the sign-in: ${ctx.out("link", started.url)}`,
12489
12591
  `Then run \`deepdock auth login --wait ${started.request}\`.`
12490
12592
  ].join(`
12491
12593
  `));
12492
12594
  }
12493
- announceApproval(ctx, "the sign-in", started.url, started.expiresAt);
12494
- ctx.rt.openBrowser(started.url);
12595
+ await announceApproval(ctx, "the sign-in", started.url, started.expiresAt, {
12596
+ noBrowser: options.browser === false
12597
+ });
12495
12598
  return finishLogin(ctx, pending);
12496
12599
  };
12497
12600
  var keyRows = (ctx, keys) => keys.map((key) => [
@@ -12602,7 +12705,19 @@ var revokeSignInById = async (ctx, id) => {
12602
12705
  throw error;
12603
12706
  }
12604
12707
  };
12605
- var logout = async (ctx) => {
12708
+ var keyLabel = (key) => `"${key.name}" (${key.id})`;
12709
+ var wantsKeysKept = async (ctx, keys, options) => {
12710
+ if (options.keepKeys)
12711
+ return true;
12712
+ if (keys.length === 0 || ctx.opts.yes || !ctx.interactive)
12713
+ return false;
12714
+ ctx.note(`Logging out revokes the API keys this CLI created (${keys.length}):`);
12715
+ for (const key of keys)
12716
+ ctx.note(` ${key.org} ${keyLabel(key)} ${key.prefix}…`);
12717
+ ctx.note("Programs using them stop working. Answer N to keep them active.");
12718
+ return !await confirm(ctx.rt, `Revoke ${keys.length === 1 ? "this key" : `these ${keys.length} keys`} too?`);
12719
+ };
12720
+ var logout = async (ctx, options) => {
12606
12721
  const config = await ctx.config();
12607
12722
  const signIn = config.signIn;
12608
12723
  const email = signIn?.email ?? null;
@@ -12610,8 +12725,10 @@ var logout = async (ctx) => {
12610
12725
  const revokedKeys = [];
12611
12726
  const failedKeys = [];
12612
12727
  let signInRevoked = false;
12728
+ const candidates = config.keys.filter((entry) => !entry.kept);
12729
+ const keepKeys = usable ? await wantsKeysKept(ctx, candidates, options) : Boolean(options.keepKeys);
12613
12730
  if (usable) {
12614
- for (const entry of [...config.keys]) {
12731
+ for (const entry of keepKeys ? [] : candidates) {
12615
12732
  const outcome = await revokeRecordedKey(ctx, entry);
12616
12733
  if ("error" in outcome)
12617
12734
  failedKeys.push(outcome);
@@ -12623,7 +12740,7 @@ var logout = async (ctx) => {
12623
12740
  signInRevoked = true;
12624
12741
  }
12625
12742
  } else {
12626
- for (const entry of config.keys) {
12743
+ for (const entry of keepKeys ? [] : candidates) {
12627
12744
  failedKeys.push({
12628
12745
  ...factsOf(entry),
12629
12746
  error: { code: "sign_in_needed", message: "Your sign-in is missing or expired." }
@@ -12635,27 +12752,40 @@ var logout = async (ctx) => {
12635
12752
  current.defaultOrg = undefined;
12636
12753
  current.organisations = undefined;
12637
12754
  current.pending = [];
12755
+ if (keepKeys) {
12756
+ for (const entry of current.keys)
12757
+ entry.kept = true;
12758
+ }
12638
12759
  });
12760
+ const signedOut = paint(ctx.out, "success", "Signed out");
12761
+ const keptKeys = (await ctx.config()).keys.filter((entry) => entry.kept).map((entry) => ({
12762
+ ...factsOf(entry),
12763
+ written: { file: displayPath(ctx.rt, entry.file), name: entry.variable }
12764
+ }));
12639
12765
  const lines = [];
12640
12766
  if (!email)
12641
12767
  lines.push("Not signed in.");
12642
12768
  else if (signInRevoked)
12643
- lines.push(`Signed out ${email}. The sign-in is revoked.`);
12769
+ lines.push(`${signedOut} ${email}. The sign-in is revoked.`);
12644
12770
  else if (usable) {
12645
- lines.push(`Signed out ${email}. The stored sign-in has no id (the settings file is damaged), so it was only removed from this computer; revoke it in the Deepdock dashboard.`);
12771
+ lines.push(`${signedOut} ${email}. The stored sign-in has no id (the settings file is damaged), so it was only removed from this computer; revoke it in the Deepdock dashboard.`);
12646
12772
  } else {
12647
- lines.push(`Signed out ${email}. The sign-in was only removed from this computer; revoke it in the Deepdock dashboard.`);
12773
+ lines.push(`${signedOut} ${email}. The sign-in was only removed from this computer; revoke it in the Deepdock dashboard.`);
12648
12774
  }
12649
12775
  if (revokedKeys.length > 0) {
12650
- lines.push("Revoked the API keys this CLI created:");
12776
+ lines.push(`${paint(ctx.out, "success", "Revoked")} the API keys this CLI created:`);
12651
12777
  for (const key of revokedKeys) {
12652
12778
  const where = key.removed.removal === "done" ? ` ${describeRemoval(key.removed)}` : "";
12653
12779
  lines.push(` ${key.org} "${key.name}" ${key.prefix}… ${key.id}${where}`);
12654
12780
  }
12655
12781
  lines.push("Programs still using one of them stop working now.");
12656
12782
  }
12783
+ if (keptKeys.length > 0) {
12784
+ lines.push(`${paint(ctx.out, "success", "Kept")} ${keptKeys.length} API ${keptKeys.length === 1 ? "key" : "keys"} active: ${keptKeys.map(keyLabel).join(", ")}`);
12785
+ lines.push("Revoke one later with `deepdock keys revoke <id>` after `deepdock auth login`.");
12786
+ }
12657
12787
  for (const key of failedKeys) {
12658
- lines.push(`Could not revoke "${key.name}" (${key.id}): run \`deepdock keys revoke ${key.id}\` after \`deepdock auth login\`.`);
12788
+ lines.push(`${paint(ctx.out, "error", "Could not revoke")} "${key.name}" (${key.id}): run \`deepdock keys revoke ${key.id}\` after \`deepdock auth login\`.`);
12659
12789
  }
12660
12790
  const hints = [
12661
12791
  ...failedKeys.length > 0 ? ["Revoke the remaining keys with `deepdock keys revoke <id>`."] : [],
@@ -12666,6 +12796,7 @@ var logout = async (ctx) => {
12666
12796
  revoked: signInRevoked,
12667
12797
  email,
12668
12798
  apiKeysRevoked: revokedKeys,
12799
+ apiKeysKept: keptKeys,
12669
12800
  ...failedKeys.length > 0 ? { apiKeysNotRevoked: failedKeys } : {}
12670
12801
  },
12671
12802
  meta: hints.length > 0 ? { hint: hints.join(" ") } : {},
@@ -12675,38 +12806,10 @@ var logout = async (ctx) => {
12675
12806
  };
12676
12807
  var registerAuth = (program, run) => {
12677
12808
  const auth = program.command("auth").description("Sign in to Deepdock as yourself, see who is signed in, sign out");
12678
- const loginCommand = auth.command("login").description("Sign in to Deepdock as yourself").option("--wait <request>", "finish a request started with --no-wait").option("--no-wait", "print the request and exit").action((options, cmd) => run(cmd, (ctx) => login(ctx, options)));
12809
+ const loginCommand = auth.command("login").description("Sign in to Deepdock as yourself").option("--wait <request>", "finish a request started with --no-wait").option("--no-wait", "print the request and exit").option("--no-browser", "do not open the link in a browser").action((options, cmd) => run(cmd, (ctx) => login(ctx, options)));
12679
12810
  loginCommand.helpInformation = () => AUTH_LOGIN_HELP;
12680
12811
  auth.command("status").description("Show who is signed in").action((_options, cmd) => run(cmd, status));
12681
- auth.command("logout").description("Revoke the sign-in and the API keys this CLI created").action((_options, cmd) => run(cmd, logout));
12682
- };
12683
-
12684
- // src/output/confirm.ts
12685
- import { createInterface } from "node:readline";
12686
- var ask = (rt, prompt) => new Promise((resolve) => {
12687
- const rl = createInterface({ input: rt.stdin.stream, output: undefined, terminal: false });
12688
- rt.stderr.write(prompt);
12689
- let settled = false;
12690
- rl.once("line", (line) => {
12691
- settled = true;
12692
- rl.close();
12693
- resolve(line.trim());
12694
- });
12695
- rl.once("close", () => {
12696
- if (!settled)
12697
- resolve("");
12698
- });
12699
- });
12700
- var confirm = async (rt, question) => {
12701
- const answer = await ask(rt, `${question} [y/N]: `);
12702
- return /^y(es)?$/i.test(answer);
12703
- };
12704
- var pickIndex = async (rt, count, defaultIndex) => {
12705
- const answer = await ask(rt, `Number [${defaultIndex}]: `);
12706
- const chosen = answer === "" ? defaultIndex : Number.parseInt(answer, 10);
12707
- if (!Number.isInteger(chosen) || chosen < 1 || chosen > count)
12708
- return;
12709
- return chosen;
12812
+ auth.command("logout").description("Revoke the sign-in and the API keys this CLI created").option("--keep-keys", "revoke only the sign-in and leave the API keys active").action((options, cmd) => run(cmd, (ctx) => logout(ctx, options)));
12710
12813
  };
12711
12814
 
12712
12815
  // src/commands/devices.ts
@@ -12751,7 +12854,9 @@ var pair = async (ctx, code, label) => {
12751
12854
  const device = isRecord(response.data) ? response.data : {};
12752
12855
  const id = field(device, "id");
12753
12856
  const name = field(device, "label");
12754
- const lines = [`Paired device ${id}${name ? ` (${stripControl(name)})` : ""}.`];
12857
+ const lines = [
12858
+ `${paint(ctx.out, "success", "Paired")} device ${id}${name ? ` (${stripControl(name)})` : ""}.`
12859
+ ];
12755
12860
  const status = field(device, "status");
12756
12861
  if (status)
12757
12862
  lines.push(` Status: ${status}`);
@@ -12773,7 +12878,7 @@ var rename = async (ctx, id, label) => {
12773
12878
  return {
12774
12879
  data: response.data,
12775
12880
  meta: meta(response.requestId),
12776
- text: `Renamed device ${deviceId} to ${stripControl(label)}.`
12881
+ text: `${paint(ctx.out, "success", "Renamed")} device ${deviceId} to ${stripControl(label)}.`
12777
12882
  };
12778
12883
  };
12779
12884
  var release = async (ctx, id) => {
@@ -12802,7 +12907,7 @@ var release = async (ctx, id) => {
12802
12907
  return {
12803
12908
  data: response.data,
12804
12909
  meta: meta(response.requestId),
12805
- text: `Released device ${deviceId}.`
12910
+ text: `${paint(ctx.out, "success", "Released")} device ${deviceId}.`
12806
12911
  };
12807
12912
  };
12808
12913
  var renderSystems = (ctx, data) => {
@@ -12925,13 +13030,14 @@ var finishKey = async (ctx, pending, warned) => {
12925
13030
  const file = displayPath(ctx.rt, pending.file);
12926
13031
  const days = Math.round((Date.parse(created.expiresAt) - ctx.rt.now().getTime()) / DAY_MS);
12927
13032
  const text = [
12928
- `Created API key "${stripControl(created.name)}".`,
13033
+ `${paint(ctx.out, "success", "Created")} API key "${stripControl(created.name)}".`,
12929
13034
  ` ID: ${created.id}`,
12930
13035
  ` Prefix: ${created.keyPrefix}`,
12931
13036
  ` Expires: ${created.expiresAt.slice(0, 10)} (${days} days)`,
12932
13037
  ` Written: ${file} (${pending.variable})`,
12933
13038
  `The key value was shown to nobody and this CLI keeps no copy; your code reads it from ${file}.`,
12934
- `Revoke it when you ship: \`deepdock keys revoke ${created.id}\`.`
13039
+ `To revoke it: ${paint(ctx.out, "emphasis", `deepdock keys revoke ${created.id}`)}`,
13040
+ `${paint(ctx.out, "emphasis", "`deepdock auth logout`")} revokes this key; use ${paint(ctx.out, "emphasis", "`auth logout --keep-keys`")} to keep it.`
12935
13041
  ].join(`
12936
13042
  `);
12937
13043
  return {
@@ -12953,7 +13059,7 @@ var create = async (ctx, name, options) => {
12953
13059
  if (!pending) {
12954
13060
  throw usageError(`There is no pending key request ${options.wait}.`, "Start one with `deepdock keys create ... --no-wait`.");
12955
13061
  }
12956
- ctx.note("Waiting for approval in the browser.");
13062
+ noteWaiting(ctx);
12957
13063
  return finishKey(ctx, pending, false);
12958
13064
  }
12959
13065
  if (name === undefined || name.trim() === "" || name.length > 100 || stripControl(name) !== name) {
@@ -12990,13 +13096,15 @@ var create = async (ctx, name, options) => {
12990
13096
  await upsertPending(ctx, pending);
12991
13097
  if (options.wait === false || !ctx.interactive) {
12992
13098
  return pendingResult(pending, started.url, [
12993
- `Open this link to approve the key: ${started.url}`,
13099
+ `Open this link to approve the key: ${ctx.out("link", started.url)}`,
12994
13100
  `Then run \`deepdock keys create --wait ${started.request}\`.`
12995
13101
  ].join(`
12996
13102
  `));
12997
13103
  }
12998
- announceApproval(ctx, "the key", started.url, started.expiresAt, `A human approves "Create an API key in ${org.name}?" there; the page shows this computer's name.`);
12999
- ctx.rt.openBrowser(started.url);
13104
+ await announceApproval(ctx, "the key", started.url, started.expiresAt, {
13105
+ extra: `A human approves ${paint(ctx.err, "emphasis", `"Create an API key in ${org.name}?"`)} there; the page shows this computer's name.`,
13106
+ noBrowser: options.browser === false
13107
+ });
13000
13108
  return finishKey(ctx, pending, true);
13001
13109
  };
13002
13110
  var revoke = async (ctx, id) => {
@@ -13038,7 +13146,7 @@ Every program using it stops working at once.`;
13038
13146
  const removed = entry ? await settleWrittenLine(ctx, entry, true) : null;
13039
13147
  const apiData = isRecord(response.data) ? response.data : {};
13040
13148
  const label = entry ? `API key "${stripControl(entry.name)}"` : `API key ${keyId}`;
13041
- const text = removed?.removal === "done" ? `Revoked ${label} and ${describeRemoval(removed)}.` : `Revoked ${label}.`;
13149
+ const text = removed?.removal === "done" ? `${paint(ctx.out, "success", "Revoked")} ${label} and ${describeRemoval(removed)}.` : `${paint(ctx.out, "success", "Revoked")} ${label}.`;
13042
13150
  return {
13043
13151
  data: { ...apiData, ...alreadyRevoked ? { alreadyRevoked: true } : {}, removed },
13044
13152
  meta: { requestId: response.requestId },
@@ -13049,7 +13157,7 @@ var registerKeys = (program, run) => {
13049
13157
  const keys = program.command("keys").description("Create, list and revoke your organisation's API keys");
13050
13158
  keys.command("list").description("List the API keys of the organisation").action((_options, cmd) => run(cmd, list2));
13051
13159
  const allowed = Object.keys(EXPIRY_DAYS).join("|");
13052
- const createCommand = keys.command("create").description("Create an API key for your organisation (admin)").argument("[name]", "a name that says what the key is for").option("--expires <duration>", `${allowed}; 90 days is the maximum from the CLI`).option("--write-env <file>", "write DEEPDOCK_API_KEY=<key> into this env file").option("--wait <request>", "finish a request started with --no-wait").option("--no-wait", "print the request and exit").action((name, options, cmd) => run(cmd, (ctx) => create(ctx, name, options)));
13160
+ const createCommand = keys.command("create").description("Create an API key for your organisation (admin)").argument("[name]", "a name that says what the key is for").option("--expires <duration>", `${allowed}; 90 days is the maximum from the CLI`).option("--write-env <file>", "write DEEPDOCK_API_KEY=<key> into this env file").option("--wait <request>", "finish a request started with --no-wait").option("--no-wait", "print the request and exit").option("--no-browser", "do not open the link in a browser").action((name, options, cmd) => run(cmd, (ctx) => create(ctx, name, options)));
13053
13161
  createCommand.helpInformation = () => KEYS_CREATE_HELP;
13054
13162
  keys.command("revoke").description("Revoke an API key (admin)").argument("<id>", "the key id").action((id, _options, cmd) => run(cmd, (ctx) => revoke(ctx, id)));
13055
13163
  };
@@ -13118,7 +13226,7 @@ var select = async (ctx, slug) => {
13118
13226
  return {
13119
13227
  data: { slug: found.slug, name: found.name, role: found.role },
13120
13228
  meta: response.requestId ? { requestId: response.requestId } : {},
13121
- text: `Default organisation: ${found.name} (${found.slug}).`
13229
+ text: `${paint(ctx.out, "success", "Default organisation:")} ${found.name} (${found.slug}).`
13122
13230
  };
13123
13231
  };
13124
13232
  var registerOrg = (program, run) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deepdockai/cli",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "Deepdock partner CLI: pair devices, see systems and actions, create the API key your code runs them with.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -23,6 +23,8 @@
23
23
  },
24
24
  "scripts": {
25
25
  "build": "bun scripts/build.ts",
26
+ "dev": "bun scripts/dev.ts",
27
+ "pack:try": "bun scripts/packTry.ts",
26
28
  "format": "biome format .",
27
29
  "format:fix": "biome format --write .",
28
30
  "lint": "biome lint .",
@@ -42,8 +42,8 @@ in the dashboard" and stop retrying.
42
42
  - Never ask for, read, print or paste a key value, and never try to extract it from the file.
43
43
  Code reads it at runtime from where the CLI put it.
44
44
  - A development key reaches every device of the organisation until it expires or is revoked; keep
45
- the expiry short (`30d`), use it only against the demo device, and tell the human to revoke it
46
- (`deepdock keys revoke <id>`) when the integration ships. The live key is created the same way,
45
+ the expiry short (`30d`), use it only against the demo device, and tell the human its expiry
46
+ date and that `deepdock keys revoke <id>` ends it. The live key is created the same way,
47
47
  by the human from their own CLI, into the live secret store.
48
48
 
49
49
  ## The CLI sign-in (CLI only)
@@ -54,7 +54,7 @@ permissions. The sign-in belongs to the person, not to an organisation: every co
54
54
  `--org <slug>`. It does what that person can do in the dashboard (members read, pair and rename;
55
55
  admins also release devices, create and revoke API keys) and never runs an action.
56
56
  `deepdock auth status` shows the signed-in person, organisations and expiry (exit 5 when not signed in); `deepdock auth
57
- logout` revokes the sign-in and every API key the CLI created.
57
+ logout` revokes the sign-in and every API key the CLI created, unless `--keep-keys` is passed (it then revokes only the sign-in). On a terminal it lists the keys and asks first; with `--json` or without a terminal it revokes them unless `--keep-keys` is passed.
58
58
 
59
59
  Without a terminal `auth login` prints the request and exits; show the human the link, wait for
60
60
  them to approve, then run `deepdock auth login --wait <request>`. Do not try to answer for them.
@@ -14,7 +14,7 @@ the API key the CLI wrote to the app's env file.
14
14
  |---|---|---|
15
15
  | `auth login` | opens a link to the dashboard where the human clicks Approve; prints `Signed in as <email>.` and the person's organisations; the sign-in lasts 90 days | anyone |
16
16
  | `auth status` | signed-in person, sign-in expiry, organisations, default organisation, the API keys this CLI created (id, prefix, expiry, where it was written); prints the same document and exits 5 when not signed in | anyone |
17
- | `auth logout` | revokes the sign-in and every API key this CLI created, removes the entries it wrote, says what it revoked | anyone |
17
+ | `auth logout [--keep-keys]` | revokes the sign-in and, unless `--keep-keys`, every API key this CLI created (on a terminal it lists them and asks, default keep; with `--json` or no terminal it revokes them unless `--keep-keys`); removes the entries it wrote, says what it revoked or kept | anyone |
18
18
  | `org list` | the person's organisations: slug, name, role, which one is the default | member |
19
19
  | `org select [slug]` | sets the default organisation; without a slug, a pick-list on a terminal | member |
20
20
  | `systems list` | the systems and actions assigned to the organisation | member |
@@ -32,11 +32,13 @@ the API key the CLI wrote to the app's env file.
32
32
  | `skill [--agent claude\|codex\|cursor\|all]` | writes this skill into the current repository | anyone |
33
33
 
34
34
  Every command that acts in an organisation uses `--org <slug>` after the command, else
35
- `DEEPDOCK_ORG`, else the default from `org select`; with none it stops with `org_required` and
36
- lists the organisations. Always pass `--org` from an agent: `org select` changes the human's
35
+ `DEEPDOCK_ORG`, else the default from `org select`; if you belong to exactly one organisation it is used and named on
36
+ stderr; with several and none chosen it stops with `org_required` and lists them. Always pass `--org` from an agent: `org select` changes the human's
37
37
  default in every terminal.
38
38
 
39
- Global flags: `--org <slug>`, `--json`, `--yes`, `-h`, `-v`. Environment: `DEEPDOCK_ORG`,
39
+ `auth login` and `keys create` also open the approval link in the human's browser on a terminal, unless `--no-browser`, `DEEPDOCK_NO_BROWSER=1`, `CI` or a missing display says otherwise.
40
+
41
+ Global flags: `--org <slug>`, `--json`, `--yes`, `-h`, `-v`. Environment: `DEEPDOCK_ORG`, `DEEPDOCK_NO_BROWSER`,
40
42
  `DEEPDOCK_BASE_URL`, `NO_COLOR`.
41
43
 
42
44
  ## Waiting for the browser
@@ -60,8 +60,8 @@ The proof is your client; the CLI only shows what the device offers.
60
60
  Neither you nor the human sees the value; you work with its id, prefix and file name only.
61
61
  - Never ask for, read, print, echo, log or commit a key value. Use the key only against the demo
62
62
  device.
63
- - It reaches every device of the organisation until it expires or is revoked. Tell the human to
64
- revoke it when the integration ships and to create the live key with the same flow from their
63
+ - It reaches every device of the organisation until it expires or is revoked. Tell the human its
64
+ expiry date and that `deepdock keys revoke <id>` ends it, and that they create the live key with the same flow from their
65
65
  own CLI.
66
66
 
67
67
  ## Logs and prompts