@deepdockai/cli 1.0.2 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.3
4
+
5
+ - `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.
6
+ - `keys create` no longer advises revoking the key "when you ship"; it prints the expiry and how to revoke.
7
+ - With exactly one organisation it is used automatically (and named), and `auth login` selects it. With several and none chosen, `org_required` stays.
8
+ - 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.
9
+ - `Organisation:` is printed once per command, not again after the wait.
10
+ - Output colours by role: warning and error labels, success verbs, bold organisation and approval prompt, dim waiting line.
11
+ - 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`.
12
+
3
13
  ## 1.0.2
4
14
 
5
15
  - 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
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.3";
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",
@@ -11747,7 +11765,16 @@ var pickKey = (value) => {
11747
11765
  const { org, id, name, prefix, expiresAt, file, variable } = entry;
11748
11766
  if (!org || !id || !name || !prefix || !expiresAt || !file || !variable)
11749
11767
  return;
11750
- return { org, id, name, prefix, expiresAt, file, variable };
11768
+ return {
11769
+ org,
11770
+ id,
11771
+ name,
11772
+ prefix,
11773
+ expiresAt,
11774
+ file,
11775
+ variable,
11776
+ ...value.kept === true ? { kept: true } : {}
11777
+ };
11751
11778
  };
11752
11779
  var pickPending = (value) => {
11753
11780
  if (!isRecord3(value))
@@ -11900,11 +11927,29 @@ var updateConfig = (rt, change) => withLock(rt, async () => {
11900
11927
 
11901
11928
  // src/output/styles.ts
11902
11929
  import { styleText } from "node:util";
11930
+ var OSC = "\x1B]";
11931
+ var ST = "\x1B\\";
11932
+ var ROLES = {
11933
+ warn: "yellow",
11934
+ error: "red",
11935
+ success: "green",
11936
+ emphasis: "bold",
11937
+ quiet: "dim",
11938
+ link: "link"
11939
+ };
11903
11940
  var makeStyler = (rt, stream) => {
11904
11941
  const forced = rt.env.FORCE_COLOR !== undefined && !["", "0"].includes(rt.env.FORCE_COLOR);
11905
11942
  const enabled = (stream.isTTY || forced) && !rt.env.NO_COLOR && rt.env.TERM !== "dumb";
11906
- return (name, text) => enabled ? styleText(name, text, { validateStream: false }) : text;
11943
+ return (name, text) => {
11944
+ if (!enabled)
11945
+ return text;
11946
+ if (name !== "link")
11947
+ return styleText(name, text, { validateStream: false });
11948
+ const styled = styleText(["blue", "underline"], text, { validateStream: false });
11949
+ return `${OSC}8;;${text}${ST}${styled}${OSC}8;;${ST}`;
11950
+ };
11907
11951
  };
11952
+ var paint = (style, role, text) => style(ROLES[role], text);
11908
11953
 
11909
11954
  // src/output/terminal.ts
11910
11955
  var isInteractive = (rt, json) => rt.stdin.isTTY && rt.stdout.isTTY && !json;
@@ -11926,6 +11971,7 @@ var createContext = (rt, opts) => {
11926
11971
  const baseUrl = baseUrlFor(rt);
11927
11972
  const api = createApi(rt);
11928
11973
  let loaded;
11974
+ let announcedOrg = false;
11929
11975
  const config = async () => {
11930
11976
  loaded ??= await loadConfig(rt);
11931
11977
  return loaded;
@@ -11946,7 +11992,8 @@ var createContext = (rt, opts) => {
11946
11992
  return loaded;
11947
11993
  },
11948
11994
  note: (line) => {
11949
- rt.stderr.write(`${line}
11995
+ const warning = "Warning:";
11996
+ rt.stderr.write(`${line.startsWith(warning) ? `${paint(ctx.err, "warn", warning)}${line.slice(warning.length)}` : line}
11950
11997
  `);
11951
11998
  },
11952
11999
  signInSecret: async () => {
@@ -11978,7 +12025,10 @@ var createContext = (rt, opts) => {
11978
12025
  }
11979
12026
  const resolved = resolveOrg(requested, orgs, fromStoredDefault);
11980
12027
  ctx.org = { slug: resolved.slug, name: resolved.name };
11981
- ctx.note(ctx.err("dim", `Organisation: ${resolved.name} (${resolved.slug})`));
12028
+ if (!announcedOrg) {
12029
+ announcedOrg = true;
12030
+ 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})`)}`);
12031
+ }
11982
12032
  return resolved;
11983
12033
  },
11984
12034
  callInOrg: async (req) => {
@@ -12044,12 +12094,14 @@ an action; actions run from your code with an API key (\`deepdock keys create\`)
12044
12094
  Usage
12045
12095
  deepdock auth login [flags]
12046
12096
 
12047
- The link is printed either way; opening the browser is a convenience that may fail silently.
12097
+ The link is printed either way. On a terminal the CLI also opens it in your browser, unless
12098
+ CI is set, there is no display, DEEPDOCK_NO_BROWSER=1 is set or you pass --no-browser.
12048
12099
 
12049
12100
  Flags
12050
12101
  --no-wait Print the request and exit; finish it later with --wait (the default
12051
12102
  when there is no terminal)
12052
12103
  --wait <request> Finish a request started with --no-wait
12104
+ --no-browser Do not open the link in a browser
12053
12105
 
12054
12106
  Examples
12055
12107
  $ deepdock auth login
@@ -12057,7 +12109,7 @@ Examples
12057
12109
  $ deepdock auth login --wait <request>
12058
12110
 
12059
12111
  More
12060
- \`deepdock auth status\` shows who is signed in (exit 5 when nobody is). \`deepdock auth logout\` revokes the sign-in.
12112
+ \`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
12113
  `;
12062
12114
  var KEYS_CREATE_HELP = `Create an API key for your organisation (admin).
12063
12115
 
@@ -12066,7 +12118,8 @@ key once, writes it into the env file named by --write-env, and keeps no copy. T
12066
12118
  never printed and the CLI never uses it; your code reads it from that file.
12067
12119
 
12068
12120
  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>\`.
12121
+ expiry short, and revoke a key you no longer need: \`deepdock keys revoke <id>\`. \`deepdock auth logout\`
12122
+ revokes the keys this CLI created unless you pass --keep-keys.
12070
12123
 
12071
12124
  Usage
12072
12125
  deepdock keys create <name> --expires <duration> --write-env <file> [flags]
@@ -12081,6 +12134,7 @@ Flags
12081
12134
  the CLI warns
12082
12135
  --no-wait Print the request and exit; finish it with --wait
12083
12136
  --wait <request> Finish a request started with --no-wait
12137
+ --no-browser Do not open the link in a browser (also DEEPDOCK_NO_BROWSER=1)
12084
12138
 
12085
12139
  Examples
12086
12140
  $ deepdock keys create "myapp dev" --expires 30d --write-env .env.local
@@ -12153,6 +12207,34 @@ var formatHelp = (command) => {
12153
12207
  `;
12154
12208
  };
12155
12209
 
12210
+ // src/output/confirm.ts
12211
+ import { createInterface } from "node:readline";
12212
+ var ask = (rt, prompt) => new Promise((resolve) => {
12213
+ const rl = createInterface({ input: rt.stdin.stream, output: undefined, terminal: false });
12214
+ rt.stderr.write(prompt);
12215
+ let settled = false;
12216
+ rl.once("line", (line) => {
12217
+ settled = true;
12218
+ rl.close();
12219
+ resolve(line.trim());
12220
+ });
12221
+ rl.once("close", () => {
12222
+ if (!settled)
12223
+ resolve("");
12224
+ });
12225
+ });
12226
+ var confirm = async (rt, question) => {
12227
+ const answer = await ask(rt, `${question} [y/N]: `);
12228
+ return /^y(es)?$/i.test(answer);
12229
+ };
12230
+ var pickIndex = async (rt, count, defaultIndex) => {
12231
+ const answer = await ask(rt, `Number [${defaultIndex}]: `);
12232
+ const chosen = answer === "" ? defaultIndex : Number.parseInt(answer, 10);
12233
+ if (!Number.isInteger(chosen) || chosen < 1 || chosen > count)
12234
+ return;
12235
+ return chosen;
12236
+ };
12237
+
12156
12238
  // src/writeEnv/ledger.ts
12157
12239
  import { relative, sep } from "node:path";
12158
12240
  var addKey = (config, entry) => {
@@ -12393,11 +12475,19 @@ var settleWrittenLine = async (ctx, entry, keepEntryOnFailure) => {
12393
12475
 
12394
12476
  // src/commands/auth.ts
12395
12477
  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.");
12478
+ var noteWaiting = (ctx) => {
12479
+ ctx.note(paint(ctx.err, "quiet", "Waiting for approval in the browser."));
12480
+ };
12481
+ var announceApproval = async (ctx, what, url, expiresAt, options) => {
12482
+ const open = browserAllowed(ctx.rt, options.noBrowser);
12483
+ const minutes = minutesLeft(ctx, expiresAt);
12484
+ 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):`);
12485
+ ctx.note(` ${ctx.err("link", url)}`);
12486
+ ctx.note(options.extra ?? "The page shows this computer's name and the CLI version: check them before you approve.");
12487
+ if (open) {
12488
+ ctx.note(await ctx.rt.openBrowser(url) ? "Opened in your browser." : "Could not open a browser; use the link above.");
12489
+ }
12490
+ noteWaiting(ctx);
12401
12491
  };
12402
12492
  var pendingResult = (pending, url, text) => ({
12403
12493
  data: { pending: { request: pending.request, url, expiresAt: pending.expiresAt } },
@@ -12441,14 +12531,22 @@ var finishLogin = async (ctx, pending) => {
12441
12531
  current.organisations = answer.organisations;
12442
12532
  if (current.defaultOrg && !slugs.includes(current.defaultOrg))
12443
12533
  current.defaultOrg = undefined;
12534
+ if (!current.defaultOrg && slugs.length === 1)
12535
+ current.defaultOrg = slugs[0];
12444
12536
  current.pending = current.pending.filter((entry) => entry.request !== pending.request);
12445
12537
  });
12446
- const lines = [`Signed in as ${answer.user.email}.`, ""];
12538
+ const lines = [`${paint(ctx.out, "success", "Signed in")} as ${answer.user.email}.`, ""];
12447
12539
  if (answer.organisations.length === 0) {
12448
12540
  lines.push("You are not a member of an organisation yet.");
12449
12541
  } else {
12450
12542
  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.");
12543
+ const only = selectableOrgs(answer.organisations);
12544
+ const [sole] = only;
12545
+ if (sole && only.length === 1 && config.defaultOrg === sole.slug) {
12546
+ lines.push(`${paint(ctx.out, "success", "Selected")} ${sole.name} (${sole.slug}) as your organisation.`);
12547
+ } else {
12548
+ lines.push("Choose one with `deepdock org select <slug>` or pass --org to each command.");
12549
+ }
12452
12550
  }
12453
12551
  return {
12454
12552
  data: {
@@ -12471,7 +12569,7 @@ var login = async (ctx, options) => {
12471
12569
  }
12472
12570
  assertSameBaseUrl(pending.baseUrl, ctx.baseUrl, "request");
12473
12571
  assertNotExpired(ctx.rt, "login", pending.expiresAt);
12474
- ctx.note("Waiting for approval in the browser.");
12572
+ noteWaiting(ctx);
12475
12573
  return finishLogin(ctx, pending);
12476
12574
  }
12477
12575
  const started = await startLogin(ctx.api, ctx.rt);
@@ -12485,13 +12583,14 @@ var login = async (ctx, options) => {
12485
12583
  await upsertPending(ctx, pending);
12486
12584
  if (options.wait === false || !ctx.interactive) {
12487
12585
  return pendingResult(pending, started.url, [
12488
- `Open this link to approve the sign-in: ${started.url}`,
12586
+ `Open this link to approve the sign-in: ${ctx.out("link", started.url)}`,
12489
12587
  `Then run \`deepdock auth login --wait ${started.request}\`.`
12490
12588
  ].join(`
12491
12589
  `));
12492
12590
  }
12493
- announceApproval(ctx, "the sign-in", started.url, started.expiresAt);
12494
- ctx.rt.openBrowser(started.url);
12591
+ await announceApproval(ctx, "the sign-in", started.url, started.expiresAt, {
12592
+ noBrowser: options.browser === false
12593
+ });
12495
12594
  return finishLogin(ctx, pending);
12496
12595
  };
12497
12596
  var keyRows = (ctx, keys) => keys.map((key) => [
@@ -12602,7 +12701,19 @@ var revokeSignInById = async (ctx, id) => {
12602
12701
  throw error;
12603
12702
  }
12604
12703
  };
12605
- var logout = async (ctx) => {
12704
+ var keyLabel = (key) => `"${key.name}" (${key.id})`;
12705
+ var wantsKeysKept = async (ctx, keys, options) => {
12706
+ if (options.keepKeys)
12707
+ return true;
12708
+ if (keys.length === 0 || ctx.opts.yes || !ctx.interactive)
12709
+ return false;
12710
+ ctx.note(`Logging out revokes the API keys this CLI created (${keys.length}):`);
12711
+ for (const key of keys)
12712
+ ctx.note(` ${key.org} ${keyLabel(key)} ${key.prefix}…`);
12713
+ ctx.note("Programs using them stop working. Answer N to keep them active.");
12714
+ return !await confirm(ctx.rt, `Revoke ${keys.length === 1 ? "this key" : `these ${keys.length} keys`} too?`);
12715
+ };
12716
+ var logout = async (ctx, options) => {
12606
12717
  const config = await ctx.config();
12607
12718
  const signIn = config.signIn;
12608
12719
  const email = signIn?.email ?? null;
@@ -12610,8 +12721,10 @@ var logout = async (ctx) => {
12610
12721
  const revokedKeys = [];
12611
12722
  const failedKeys = [];
12612
12723
  let signInRevoked = false;
12724
+ const candidates = config.keys.filter((entry) => !entry.kept);
12725
+ const keepKeys = usable ? await wantsKeysKept(ctx, candidates, options) : Boolean(options.keepKeys);
12613
12726
  if (usable) {
12614
- for (const entry of [...config.keys]) {
12727
+ for (const entry of keepKeys ? [] : candidates) {
12615
12728
  const outcome = await revokeRecordedKey(ctx, entry);
12616
12729
  if ("error" in outcome)
12617
12730
  failedKeys.push(outcome);
@@ -12623,7 +12736,7 @@ var logout = async (ctx) => {
12623
12736
  signInRevoked = true;
12624
12737
  }
12625
12738
  } else {
12626
- for (const entry of config.keys) {
12739
+ for (const entry of keepKeys ? [] : candidates) {
12627
12740
  failedKeys.push({
12628
12741
  ...factsOf(entry),
12629
12742
  error: { code: "sign_in_needed", message: "Your sign-in is missing or expired." }
@@ -12635,27 +12748,40 @@ var logout = async (ctx) => {
12635
12748
  current.defaultOrg = undefined;
12636
12749
  current.organisations = undefined;
12637
12750
  current.pending = [];
12751
+ if (keepKeys) {
12752
+ for (const entry of current.keys)
12753
+ entry.kept = true;
12754
+ }
12638
12755
  });
12756
+ const signedOut = paint(ctx.out, "success", "Signed out");
12757
+ const keptKeys = (await ctx.config()).keys.filter((entry) => entry.kept).map((entry) => ({
12758
+ ...factsOf(entry),
12759
+ written: { file: displayPath(ctx.rt, entry.file), name: entry.variable }
12760
+ }));
12639
12761
  const lines = [];
12640
12762
  if (!email)
12641
12763
  lines.push("Not signed in.");
12642
12764
  else if (signInRevoked)
12643
- lines.push(`Signed out ${email}. The sign-in is revoked.`);
12765
+ lines.push(`${signedOut} ${email}. The sign-in is revoked.`);
12644
12766
  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.`);
12767
+ 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
12768
  } else {
12647
- lines.push(`Signed out ${email}. The sign-in was only removed from this computer; revoke it in the Deepdock dashboard.`);
12769
+ lines.push(`${signedOut} ${email}. The sign-in was only removed from this computer; revoke it in the Deepdock dashboard.`);
12648
12770
  }
12649
12771
  if (revokedKeys.length > 0) {
12650
- lines.push("Revoked the API keys this CLI created:");
12772
+ lines.push(`${paint(ctx.out, "success", "Revoked")} the API keys this CLI created:`);
12651
12773
  for (const key of revokedKeys) {
12652
12774
  const where = key.removed.removal === "done" ? ` ${describeRemoval(key.removed)}` : "";
12653
12775
  lines.push(` ${key.org} "${key.name}" ${key.prefix}… ${key.id}${where}`);
12654
12776
  }
12655
12777
  lines.push("Programs still using one of them stop working now.");
12656
12778
  }
12779
+ if (keptKeys.length > 0) {
12780
+ lines.push(`${paint(ctx.out, "success", "Kept")} ${keptKeys.length} API ${keptKeys.length === 1 ? "key" : "keys"} active: ${keptKeys.map(keyLabel).join(", ")}`);
12781
+ lines.push("Revoke one later with `deepdock keys revoke <id>` after `deepdock auth login`.");
12782
+ }
12657
12783
  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\`.`);
12784
+ lines.push(`${paint(ctx.out, "error", "Could not revoke")} "${key.name}" (${key.id}): run \`deepdock keys revoke ${key.id}\` after \`deepdock auth login\`.`);
12659
12785
  }
12660
12786
  const hints = [
12661
12787
  ...failedKeys.length > 0 ? ["Revoke the remaining keys with `deepdock keys revoke <id>`."] : [],
@@ -12666,6 +12792,7 @@ var logout = async (ctx) => {
12666
12792
  revoked: signInRevoked,
12667
12793
  email,
12668
12794
  apiKeysRevoked: revokedKeys,
12795
+ apiKeysKept: keptKeys,
12669
12796
  ...failedKeys.length > 0 ? { apiKeysNotRevoked: failedKeys } : {}
12670
12797
  },
12671
12798
  meta: hints.length > 0 ? { hint: hints.join(" ") } : {},
@@ -12675,38 +12802,10 @@ var logout = async (ctx) => {
12675
12802
  };
12676
12803
  var registerAuth = (program, run) => {
12677
12804
  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)));
12805
+ 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
12806
  loginCommand.helpInformation = () => AUTH_LOGIN_HELP;
12680
12807
  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;
12808
+ 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
12809
  };
12711
12810
 
12712
12811
  // src/commands/devices.ts
@@ -12751,7 +12850,9 @@ var pair = async (ctx, code, label) => {
12751
12850
  const device = isRecord(response.data) ? response.data : {};
12752
12851
  const id = field(device, "id");
12753
12852
  const name = field(device, "label");
12754
- const lines = [`Paired device ${id}${name ? ` (${stripControl(name)})` : ""}.`];
12853
+ const lines = [
12854
+ `${paint(ctx.out, "success", "Paired")} device ${id}${name ? ` (${stripControl(name)})` : ""}.`
12855
+ ];
12755
12856
  const status = field(device, "status");
12756
12857
  if (status)
12757
12858
  lines.push(` Status: ${status}`);
@@ -12773,7 +12874,7 @@ var rename = async (ctx, id, label) => {
12773
12874
  return {
12774
12875
  data: response.data,
12775
12876
  meta: meta(response.requestId),
12776
- text: `Renamed device ${deviceId} to ${stripControl(label)}.`
12877
+ text: `${paint(ctx.out, "success", "Renamed")} device ${deviceId} to ${stripControl(label)}.`
12777
12878
  };
12778
12879
  };
12779
12880
  var release = async (ctx, id) => {
@@ -12802,7 +12903,7 @@ var release = async (ctx, id) => {
12802
12903
  return {
12803
12904
  data: response.data,
12804
12905
  meta: meta(response.requestId),
12805
- text: `Released device ${deviceId}.`
12906
+ text: `${paint(ctx.out, "success", "Released")} device ${deviceId}.`
12806
12907
  };
12807
12908
  };
12808
12909
  var renderSystems = (ctx, data) => {
@@ -12925,13 +13026,14 @@ var finishKey = async (ctx, pending, warned) => {
12925
13026
  const file = displayPath(ctx.rt, pending.file);
12926
13027
  const days = Math.round((Date.parse(created.expiresAt) - ctx.rt.now().getTime()) / DAY_MS);
12927
13028
  const text = [
12928
- `Created API key "${stripControl(created.name)}".`,
13029
+ `${paint(ctx.out, "success", "Created")} API key "${stripControl(created.name)}".`,
12929
13030
  ` ID: ${created.id}`,
12930
13031
  ` Prefix: ${created.keyPrefix}`,
12931
13032
  ` Expires: ${created.expiresAt.slice(0, 10)} (${days} days)`,
12932
13033
  ` Written: ${file} (${pending.variable})`,
12933
13034
  `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}\`.`
13035
+ `To revoke it: ${paint(ctx.out, "emphasis", `deepdock keys revoke ${created.id}`)}`,
13036
+ `${paint(ctx.out, "emphasis", "`deepdock auth logout`")} revokes this key; use ${paint(ctx.out, "emphasis", "`auth logout --keep-keys`")} to keep it.`
12935
13037
  ].join(`
12936
13038
  `);
12937
13039
  return {
@@ -12953,7 +13055,7 @@ var create = async (ctx, name, options) => {
12953
13055
  if (!pending) {
12954
13056
  throw usageError(`There is no pending key request ${options.wait}.`, "Start one with `deepdock keys create ... --no-wait`.");
12955
13057
  }
12956
- ctx.note("Waiting for approval in the browser.");
13058
+ noteWaiting(ctx);
12957
13059
  return finishKey(ctx, pending, false);
12958
13060
  }
12959
13061
  if (name === undefined || name.trim() === "" || name.length > 100 || stripControl(name) !== name) {
@@ -12990,13 +13092,15 @@ var create = async (ctx, name, options) => {
12990
13092
  await upsertPending(ctx, pending);
12991
13093
  if (options.wait === false || !ctx.interactive) {
12992
13094
  return pendingResult(pending, started.url, [
12993
- `Open this link to approve the key: ${started.url}`,
13095
+ `Open this link to approve the key: ${ctx.out("link", started.url)}`,
12994
13096
  `Then run \`deepdock keys create --wait ${started.request}\`.`
12995
13097
  ].join(`
12996
13098
  `));
12997
13099
  }
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);
13100
+ await announceApproval(ctx, "the key", started.url, started.expiresAt, {
13101
+ extra: `A human approves ${paint(ctx.err, "emphasis", `"Create an API key in ${org.name}?"`)} there; the page shows this computer's name.`,
13102
+ noBrowser: options.browser === false
13103
+ });
13000
13104
  return finishKey(ctx, pending, true);
13001
13105
  };
13002
13106
  var revoke = async (ctx, id) => {
@@ -13038,7 +13142,7 @@ Every program using it stops working at once.`;
13038
13142
  const removed = entry ? await settleWrittenLine(ctx, entry, true) : null;
13039
13143
  const apiData = isRecord(response.data) ? response.data : {};
13040
13144
  const label = entry ? `API key "${stripControl(entry.name)}"` : `API key ${keyId}`;
13041
- const text = removed?.removal === "done" ? `Revoked ${label} and ${describeRemoval(removed)}.` : `Revoked ${label}.`;
13145
+ const text = removed?.removal === "done" ? `${paint(ctx.out, "success", "Revoked")} ${label} and ${describeRemoval(removed)}.` : `${paint(ctx.out, "success", "Revoked")} ${label}.`;
13042
13146
  return {
13043
13147
  data: { ...apiData, ...alreadyRevoked ? { alreadyRevoked: true } : {}, removed },
13044
13148
  meta: { requestId: response.requestId },
@@ -13049,7 +13153,7 @@ var registerKeys = (program, run) => {
13049
13153
  const keys = program.command("keys").description("Create, list and revoke your organisation's API keys");
13050
13154
  keys.command("list").description("List the API keys of the organisation").action((_options, cmd) => run(cmd, list2));
13051
13155
  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)));
13156
+ 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
13157
  createCommand.helpInformation = () => KEYS_CREATE_HELP;
13054
13158
  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
13159
  };
@@ -13118,7 +13222,7 @@ var select = async (ctx, slug) => {
13118
13222
  return {
13119
13223
  data: { slug: found.slug, name: found.name, role: found.role },
13120
13224
  meta: response.requestId ? { requestId: response.requestId } : {},
13121
- text: `Default organisation: ${found.name} (${found.slug}).`
13225
+ text: `${paint(ctx.out, "success", "Default organisation:")} ${found.name} (${found.slug}).`
13122
13226
  };
13123
13227
  };
13124
13228
  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.3",
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",
@@ -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