@deepdockai/cli 1.0.0 → 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,25 @@
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
+
13
+ ## 1.0.2
14
+
15
+ - The install commands no longer pin a major version: `npx @deepdockai/cli auth login`, `npm install -g @deepdockai/cli`.
16
+
17
+ ## 1.0.1
18
+
19
+ - The agent skill: page with `nextCursor` until it is `null` (a short page can carry a cursor), and cover the codes `access_outdated`, `access_missing`, `record_locked`, `record_not_editable`, `unknown_provider`, `payload_too_large` and the system status `unknown`, in the skill and the CLI's exit-code table.
20
+ - The agent skill: one statement on how to fetch the OpenAPI document, and a MEDICAL OFFICE recipe with its action names.
21
+ - The agent skill states the id rule in full: reads and creates answer ids as integers, a create's new id included; every id you send is a string of digits without a leading zero, at most 10 characters, in a query string and a write body alike.
22
+
3
23
  ## 1.0.0
4
24
 
5
25
  First release of `@deepdockai/cli`, the `deepdock` command.
package/README.md CHANGED
@@ -10,14 +10,14 @@ This repository holds the partner CLI, published as [`@deepdockai/cli`](https://
10
10
  with the command `deepdock`, and the skill it installs for coding agents.
11
11
 
12
12
  ```bash
13
- npx @deepdockai/cli@1 auth login
13
+ npx @deepdockai/cli auth login
14
14
  ```
15
15
 
16
16
  The CLI manages: your organisations, the devices paired to them, and your API keys, and it lists
17
17
  the systems and actions a device offers. It never runs an action and never keeps or uses an API
18
18
  key. Actions run from your own code with a key the CLI wrote into your env file.
19
19
 
20
- Node 22.13 or newer. Installed globally (`npm install -g @deepdockai/cli@1`) the command is
20
+ Node 22.13 or newer. Installed globally (`npm install -g @deepdockai/cli`) the command is
21
21
  `deepdock`; the examples below use that form.
22
22
 
23
23
  ## Before you start
@@ -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,10 +93,10 @@ 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
- | `systems openapi <system>` | a system's OpenAPI document on stdout; redirect it to a file |
99
+ | `systems openapi <system>` | a system's OpenAPI document on stdout, bare when redirected; save it to a file |
98
100
  | `devices list` | paired devices with id, label, status, systems and last seen |
99
101
  | `devices pair <code> [--label <text>]` | claim a device by the code on its screen |
100
102
  | `devices rename <id> <label>`, `devices release <id>` | rename a device; unpair it from the organisation (admin) |
@@ -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
 
@@ -144,7 +147,9 @@ On a terminal you get tables and plain sentences. When stdout is not a terminal,
144
147
  }
145
148
  ```
146
149
 
147
- For list and show commands `data` is the API's answer unchanged, so what the CLI shows is what
150
+ `systems openapi` is the one exception off a terminal: it prints the OpenAPI document itself, so
151
+ `deepdock systems openapi <system> > openapi.json` saves a document a generator can read (`--json`
152
+ gives the envelope). For list and show commands `data` is the API's answer unchanged, so what the CLI shows is what
148
153
  your code will receive; `auth`, `org select` and `keys create` build it from named fields. A
149
154
  failure is one document on stderr, `{ "error": { "code", "message" }, "meta": { "hint" } }`,
150
155
  with the API's error object unchanged. Switch on `code` or on the exit code, never on `message`.
@@ -220,7 +225,8 @@ macOS and Linux need nothing special. On Windows:
220
225
  - **Keys expire and can be revoked.** A key reaches every device of the organisation until it
221
226
  expires (at most 90 days from the CLI) or is revoked. `deepdock keys revoke <id>` revokes it and
222
227
  removes the line the CLI wrote; `deepdock auth logout` revokes the sign-in and every key this
223
- 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
224
230
  CLI never wrote, otherwise `{ file, name, removal }` with `removal` `done`, `absent` (the line
225
231
  was already gone or holds another value) or `failed` (with a `reason`). If the file is held open
226
232
  by another program (a dev server watching it, on Windows), the key is still revoked and the
@@ -235,7 +241,7 @@ macOS and Linux need nothing special. On Windows:
235
241
  CLI created (never their values) and a pending approval request. A file other users can read
236
242
  is refused with the `chmod` to run.
237
243
  - **Environment:** `DEEPDOCK_ORG`, `DEEPDOCK_BASE_URL` (https, or http for localhost only),
238
- `NO_COLOR`. A sign-in or pending request is never sent to a different base URL than the one it
244
+ `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
239
245
  was made against.
240
246
 
241
247
  Report a vulnerability by email to torben@deepdock.ai, not in a public issue: [SECURITY.md](SECURITY.md).
@@ -260,3 +266,11 @@ DEEPDOCK_BASE_URL=http://127.0.0.1:<port> node dist/deepdock.js auth login
260
266
  ## Licence
261
267
 
262
268
  [MIT](LICENSE).
269
+
270
+ ## Releases
271
+
272
+ Publishing is automatic once a change is merged; nobody runs `npm publish`. To ship a change, bump `version` in `package.json` and add a matching
273
+ `## <version>` entry to `CHANGELOG.md` in your pull request. A check on the pull request requires
274
+ the bump whenever a published file changes. When the pull request merges to `main`, the release
275
+ workflow tests the code, scans the exact tarball, publishes it to npm with provenance, then tags
276
+ `v<version>` and creates a GitHub Release with the changelog entry as notes.
package/dist/deepdock.js CHANGED
@@ -10864,10 +10864,17 @@ var EXIT_CODES = {
10864
10864
  check_failed: 2,
10865
10865
  idempotency_conflict: 2,
10866
10866
  conflict: 2,
10867
+ already_member: 2,
10867
10868
  no_open_case: 2,
10868
10869
  unknown_author: 2,
10870
+ unknown_provider: 2,
10871
+ record_locked: 2,
10872
+ record_not_editable: 2,
10873
+ payload_too_large: 2,
10869
10874
  device_offline: 3,
10870
10875
  action_paused: 3,
10876
+ access_outdated: 3,
10877
+ access_missing: 3,
10871
10878
  system_unavailable: 3,
10872
10879
  target_busy: 3,
10873
10880
  temporarily_unavailable: 3,
@@ -10999,6 +11006,7 @@ var DEVICE_SYSTEM_COLUMNS = [
10999
11006
  "systems",
11000
11007
  "system",
11001
11008
  "name",
11009
+ "action",
11002
11010
  "version",
11003
11011
  "status",
11004
11012
  "state",
@@ -11035,12 +11043,37 @@ var scalar = (value) => {
11035
11043
  return String(value);
11036
11044
  return "";
11037
11045
  };
11046
+ var GOOD = new Set(["connected", "active", "running", "ok"]);
11047
+ var WARN = new Set(["offline", "paused", "outdated", "unavailable", "degraded"]);
11048
+ var BAD = new Set(["revoked", "failed", "denied", "expired"]);
11049
+ var cellFor = (key, value) => {
11050
+ const text = scalar(value);
11051
+ if (text === "")
11052
+ return text;
11053
+ if (key === "reason" || key === "pausedReason")
11054
+ return words(text);
11055
+ if (key === "revokedAt")
11056
+ return { text, style: "red" };
11057
+ if (key === "name" || key === "action")
11058
+ return { text, style: "bold" };
11059
+ if (key.endsWith("At") || key === "since")
11060
+ return { text, style: "dim" };
11061
+ if (key === "status" || key === "state") {
11062
+ if (GOOD.has(text))
11063
+ return { text, style: "green" };
11064
+ if (WARN.has(text))
11065
+ return { text, style: "yellow" };
11066
+ if (BAD.has(text))
11067
+ return { text, style: "red" };
11068
+ }
11069
+ return text;
11070
+ };
11038
11071
  var genericTable = (items, style, allow) => {
11039
11072
  const records = items.filter(isRecord);
11040
11073
  const shown = allow.filter((key) => records.some((record) => (key in record) && isScalar(record[key])));
11041
11074
  if (shown.length === 0)
11042
11075
  return NOTHING_TO_SHOW;
11043
- return renderTable(shown.map(columnTitle), records.map((record) => shown.map((key) => scalar(record[key]))), style);
11076
+ return renderTable(shown.map(columnTitle), records.map((record) => shown.map((key) => cellFor(key, record[key]))), style);
11044
11077
  };
11045
11078
  var relativeTime = (iso, now) => {
11046
11079
  if (typeof iso !== "string")
@@ -11084,10 +11117,10 @@ var deviceTable = (devices, now, style) => {
11084
11117
  return genericTable(records, style, DEVICE_COLUMNS);
11085
11118
  return renderTable(["ID", "LABEL", "STATUS", "SYSTEMS", "LAST SEEN"], records.map((device) => [
11086
11119
  field(device, "id"),
11087
- field(device, "label"),
11120
+ { text: field(device, "label"), style: "bold" },
11088
11121
  deviceStatusCell(field(device, "status")),
11089
11122
  systemsLabel(device),
11090
- relativeTime(device.lastSeenAt, now)
11123
+ { text: relativeTime(device.lastSeenAt, now), style: "dim" }
11091
11124
  ]), style);
11092
11125
  };
11093
11126
  var isScalar = (value) => value === null || ["string", "number", "boolean"].includes(typeof value);
@@ -11151,6 +11184,32 @@ var countValue = (data, word) => {
11151
11184
  return Object.values(data).reduce((sum, item) => sum + countValue(item, word), 0);
11152
11185
  return 0;
11153
11186
  };
11187
+ var roleLabel = (role) => {
11188
+ const bare = role.replace(/^org:/, "").replace(/[_-]+/g, " ");
11189
+ return bare === "" ? role : bare.charAt(0).toUpperCase() + bare.slice(1);
11190
+ };
11191
+ var orgTable = (orgs, defaultSlug, style) => {
11192
+ const isDefault = (org) => org.slug !== null && org.slug === defaultSlug;
11193
+ const ordered = [...orgs].sort((a, b) => Number(isDefault(b)) - Number(isDefault(a)));
11194
+ return renderTable(["NAME", "SLUG", "ROLE", ""], ordered.map((org) => [
11195
+ { text: org.name, style: "bold" },
11196
+ { text: org.slug ?? "(no slug)", style: "dim" },
11197
+ roleLabel(org.role),
11198
+ isDefault(org) ? { text: "default", style: "green" } : ""
11199
+ ]), style);
11200
+ };
11201
+ var words = (code) => code.replace(/_/g, " ");
11202
+ var systemsTable = (items, style) => {
11203
+ const records = items.filter(isRecord);
11204
+ if (!records.some((item) => ("system" in item)))
11205
+ return genericTable(records, style, SYSTEM_COLUMNS);
11206
+ return renderTable(["SYSTEM", "NAME", "ACTIONS"], records.map((item) => {
11207
+ const actions = Array.isArray(item.actions) ? item.actions.filter(isRecord) : undefined;
11208
+ const writes = actions?.filter((action) => action.kind === "write").length ?? 0;
11209
+ const counted = actions ? `${actions.length} (${actions.length - writes} read, ${writes} write)` : "";
11210
+ return [{ text: field(item, "system"), style: "bold" }, field(item, "label"), counted];
11211
+ }), style);
11212
+ };
11154
11213
 
11155
11214
  // src/commands/actions.ts
11156
11215
  var fieldNames = (input) => {
@@ -11159,14 +11218,25 @@ var fieldNames = (input) => {
11159
11218
  const required = Array.isArray(input.required) ? input.required : [];
11160
11219
  return Object.keys(input.properties).map((name) => required.includes(name) ? name : `${name} (optional)`).join(", ");
11161
11220
  };
11221
+ var summary = (description) => {
11222
+ if (typeof description !== "string")
11223
+ return;
11224
+ const line = description.split(`
11225
+ `).find((text) => text.trim() !== "")?.trim() ?? "";
11226
+ const sentence = /^.*?[.!?](?=\s+\p{Lu}|$)/u.exec(line)?.[0] ?? line;
11227
+ const chars = Array.from(sentence);
11228
+ return chars.length > 160 ? `${chars.slice(0, 159).join("").trimEnd()}…` : sentence;
11229
+ };
11162
11230
  var actionRow = (action) => {
11163
11231
  const method = typeof action.method === "string" ? action.method : undefined;
11164
11232
  const fields = fieldNames(action.input);
11165
- const { name, method: _method, input: _input, output: _output, ...rest } = action;
11233
+ const { name, method: _method, input: _input, output: _output, description, ...rest } = action;
11234
+ const brief = summary(description);
11166
11235
  return {
11167
11236
  ...name === undefined ? {} : { name },
11168
11237
  ...method ? { kind: method.toUpperCase() === "GET" ? "read" : "write" } : {},
11169
11238
  ...fields ? { fields } : {},
11239
+ ...brief ? { description: brief } : {},
11170
11240
  ...rest
11171
11241
  };
11172
11242
  };
@@ -11253,6 +11323,18 @@ var parseAnswer = async (name, value) => {
11253
11323
  return result;
11254
11324
  };
11255
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
+
11256
11338
  // src/approval/poll.ts
11257
11339
  var MAX_CONSECUTIVE_FAILURES = 5;
11258
11340
  var EXPIRY_GRACE_MS = 15000;
@@ -11342,7 +11424,7 @@ import { spawn } from "node:child_process";
11342
11424
  import { homedir, hostname } from "node:os";
11343
11425
 
11344
11426
  // src/version.ts
11345
- var VERSION = "1.0.0";
11427
+ var VERSION = "1.0.3";
11346
11428
 
11347
11429
  // src/runtime.ts
11348
11430
  var DEFAULT_BASE_URL = "https://api.deepdock.ai";
@@ -11353,16 +11435,19 @@ var browserCommand = (platform, url) => {
11353
11435
  return { command: "rundll32", args: ["url.dll,FileProtocolHandler", url] };
11354
11436
  return { command: "xdg-open", args: [url] };
11355
11437
  };
11356
- var openInBrowser = (url) => {
11438
+ var openInBrowser = (url) => new Promise((resolve) => {
11357
11439
  const { command, args } = browserCommand(process.platform, url);
11358
11440
  try {
11359
11441
  const child = spawn(command, args, { stdio: "ignore", detached: true });
11360
- child.on("error", () => {
11361
- return;
11442
+ child.once("error", () => resolve(false));
11443
+ child.once("spawn", () => {
11444
+ child.unref();
11445
+ resolve(true);
11362
11446
  });
11363
- child.unref();
11364
- } catch {}
11365
- };
11447
+ } catch {
11448
+ resolve(false);
11449
+ }
11450
+ });
11366
11451
  var defaultRuntime = () => {
11367
11452
  const interrupt = new AbortController;
11368
11453
  process.once("SIGINT", () => interrupt.abort());
@@ -11568,6 +11653,9 @@ var requestedSlug = (flag, env, config) => flag || env.DEEPDOCK_ORG || config.de
11568
11653
  var resolveOrg = (requested, orgs, fromStoredDefault = false) => {
11569
11654
  const choices = selectableOrgs(orgs);
11570
11655
  const slugs = choices.map((org) => org.slug);
11656
+ const [only] = choices;
11657
+ if (!requested && only && choices.length === 1)
11658
+ return { ...only, onlyOne: true };
11571
11659
  if (!requested) {
11572
11660
  throw new CliError({
11573
11661
  code: "org_required",
@@ -11677,7 +11765,16 @@ var pickKey = (value) => {
11677
11765
  const { org, id, name, prefix, expiresAt, file, variable } = entry;
11678
11766
  if (!org || !id || !name || !prefix || !expiresAt || !file || !variable)
11679
11767
  return;
11680
- 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
+ };
11681
11778
  };
11682
11779
  var pickPending = (value) => {
11683
11780
  if (!isRecord3(value))
@@ -11830,10 +11927,29 @@ var updateConfig = (rt, change) => withLock(rt, async () => {
11830
11927
 
11831
11928
  // src/output/styles.ts
11832
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
+ };
11833
11940
  var makeStyler = (rt, stream) => {
11834
- const enabled = stream.isTTY && !rt.env.NO_COLOR && rt.env.TERM !== "dumb";
11835
- return (name, text) => enabled ? styleText(name, text, { validateStream: false }) : text;
11941
+ const forced = rt.env.FORCE_COLOR !== undefined && !["", "0"].includes(rt.env.FORCE_COLOR);
11942
+ const enabled = (stream.isTTY || forced) && !rt.env.NO_COLOR && rt.env.TERM !== "dumb";
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
+ };
11836
11951
  };
11952
+ var paint = (style, role, text) => style(ROLES[role], text);
11837
11953
 
11838
11954
  // src/output/terminal.ts
11839
11955
  var isInteractive = (rt, json) => rt.stdin.isTTY && rt.stdout.isTTY && !json;
@@ -11855,6 +11971,7 @@ var createContext = (rt, opts) => {
11855
11971
  const baseUrl = baseUrlFor(rt);
11856
11972
  const api = createApi(rt);
11857
11973
  let loaded;
11974
+ let announcedOrg = false;
11858
11975
  const config = async () => {
11859
11976
  loaded ??= await loadConfig(rt);
11860
11977
  return loaded;
@@ -11875,7 +11992,8 @@ var createContext = (rt, opts) => {
11875
11992
  return loaded;
11876
11993
  },
11877
11994
  note: (line) => {
11878
- 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}
11879
11997
  `);
11880
11998
  },
11881
11999
  signInSecret: async () => {
@@ -11907,7 +12025,10 @@ var createContext = (rt, opts) => {
11907
12025
  }
11908
12026
  const resolved = resolveOrg(requested, orgs, fromStoredDefault);
11909
12027
  ctx.org = { slug: resolved.slug, name: resolved.name };
11910
- 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
+ }
11911
12032
  return resolved;
11912
12033
  },
11913
12034
  callInOrg: async (req) => {
@@ -11958,7 +12079,7 @@ Examples
11958
12079
 
11959
12080
  More
11960
12081
  Exit codes: \`deepdock help exit-codes\`. API reference per system: \`deepdock systems openapi <system>\`.
11961
- Environment: DEEPDOCK_ORG, DEEPDOCK_BASE_URL, NO_COLOR.
12082
+ Environment: DEEPDOCK_ORG, DEEPDOCK_BASE_URL, NO_COLOR, FORCE_COLOR.
11962
12083
  Actions run with an API key from your own code, never from this CLI: \`deepdock keys create --help\`.
11963
12084
  `;
11964
12085
  var AUTH_LOGIN_HELP = `Sign in to Deepdock as yourself.
@@ -11973,12 +12094,14 @@ an action; actions run from your code with an API key (\`deepdock keys create\`)
11973
12094
  Usage
11974
12095
  deepdock auth login [flags]
11975
12096
 
11976
- 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.
11977
12099
 
11978
12100
  Flags
11979
12101
  --no-wait Print the request and exit; finish it later with --wait (the default
11980
12102
  when there is no terminal)
11981
12103
  --wait <request> Finish a request started with --no-wait
12104
+ --no-browser Do not open the link in a browser
11982
12105
 
11983
12106
  Examples
11984
12107
  $ deepdock auth login
@@ -11986,7 +12109,7 @@ Examples
11986
12109
  $ deepdock auth login --wait <request>
11987
12110
 
11988
12111
  More
11989
- \`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.
11990
12113
  `;
11991
12114
  var KEYS_CREATE_HELP = `Create an API key for your organisation (admin).
11992
12115
 
@@ -11995,7 +12118,8 @@ key once, writes it into the env file named by --write-env, and keeps no copy. T
11995
12118
  never printed and the CLI never uses it; your code reads it from that file.
11996
12119
 
11997
12120
  A key reaches every device of the organisation until it expires or is revoked. Keep the
11998
- 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.
11999
12123
 
12000
12124
  Usage
12001
12125
  deepdock keys create <name> --expires <duration> --write-env <file> [flags]
@@ -12010,6 +12134,7 @@ Flags
12010
12134
  the CLI warns
12011
12135
  --no-wait Print the request and exit; finish it with --wait
12012
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)
12013
12138
 
12014
12139
  Examples
12015
12140
  $ deepdock keys create "myapp dev" --expires 30d --write-env .env.local
@@ -12082,6 +12207,34 @@ var formatHelp = (command) => {
12082
12207
  `;
12083
12208
  };
12084
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
+
12085
12238
  // src/writeEnv/ledger.ts
12086
12239
  import { relative, sep } from "node:path";
12087
12240
  var addKey = (config, entry) => {
@@ -12322,11 +12475,19 @@ var settleWrittenLine = async (ctx, entry, keepEntryOnFailure) => {
12322
12475
 
12323
12476
  // src/commands/auth.ts
12324
12477
  var minutesLeft = (ctx, expiresAt) => Math.max(1, Math.ceil((Date.parse(expiresAt) - ctx.rt.now().getTime()) / 60000));
12325
- var announceApproval = (ctx, what, url, expiresAt, extra) => {
12326
- ctx.note(`Open this link to approve ${what} (it expires in ${minutesLeft(ctx, expiresAt)} minutes):`);
12327
- ctx.note(` ${url}`);
12328
- ctx.note(extra ?? "The page shows this computer's name and the CLI version: check them before you approve.");
12329
- 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);
12330
12491
  };
12331
12492
  var pendingResult = (pending, url, text) => ({
12332
12493
  data: { pending: { request: pending.request, url, expiresAt: pending.expiresAt } },
@@ -12340,10 +12501,6 @@ var upsertPending = async (ctx, pending) => {
12340
12501
  ];
12341
12502
  });
12342
12503
  };
12343
- var orgRows = (orgs) => orgs.map((org) => [org.slug ?? "(no slug)", org.name, org.role]);
12344
- var orgTable = (ctx, orgs) => renderTable(["SLUG", "NAME", "ROLE"], orgRows(orgs), ctx.out).split(`
12345
- `).map((line) => ` ${line}`).join(`
12346
- `);
12347
12504
  var finishLogin = async (ctx, pending) => {
12348
12505
  const { answer, requestId } = await pollUntilApproved(ctx.rt, ctx.api, "loginPoll", {
12349
12506
  kind: "login",
@@ -12374,14 +12531,22 @@ var finishLogin = async (ctx, pending) => {
12374
12531
  current.organisations = answer.organisations;
12375
12532
  if (current.defaultOrg && !slugs.includes(current.defaultOrg))
12376
12533
  current.defaultOrg = undefined;
12534
+ if (!current.defaultOrg && slugs.length === 1)
12535
+ current.defaultOrg = slugs[0];
12377
12536
  current.pending = current.pending.filter((entry) => entry.request !== pending.request);
12378
12537
  });
12379
- const lines = [`Signed in as ${answer.user.email}.`, ""];
12538
+ const lines = [`${paint(ctx.out, "success", "Signed in")} as ${answer.user.email}.`, ""];
12380
12539
  if (answer.organisations.length === 0) {
12381
12540
  lines.push("You are not a member of an organisation yet.");
12382
12541
  } else {
12383
- lines.push("Your organisations:", orgTable(ctx, answer.organisations), "");
12384
- lines.push("Choose one with `deepdock org select <slug>` or pass --org to each command.");
12542
+ lines.push("Your organisations:", indented(orgTable(answer.organisations, config.defaultOrg, ctx.out)), "");
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
+ }
12385
12550
  }
12386
12551
  return {
12387
12552
  data: {
@@ -12404,6 +12569,7 @@ var login = async (ctx, options) => {
12404
12569
  }
12405
12570
  assertSameBaseUrl(pending.baseUrl, ctx.baseUrl, "request");
12406
12571
  assertNotExpired(ctx.rt, "login", pending.expiresAt);
12572
+ noteWaiting(ctx);
12407
12573
  return finishLogin(ctx, pending);
12408
12574
  }
12409
12575
  const started = await startLogin(ctx.api, ctx.rt);
@@ -12417,13 +12583,14 @@ var login = async (ctx, options) => {
12417
12583
  await upsertPending(ctx, pending);
12418
12584
  if (options.wait === false || !ctx.interactive) {
12419
12585
  return pendingResult(pending, started.url, [
12420
- `Open this link to approve the sign-in: ${started.url}`,
12586
+ `Open this link to approve the sign-in: ${ctx.out("link", started.url)}`,
12421
12587
  `Then run \`deepdock auth login --wait ${started.request}\`.`
12422
12588
  ].join(`
12423
12589
  `));
12424
12590
  }
12425
- announceApproval(ctx, "the sign-in", started.url, started.expiresAt);
12426
- ctx.rt.openBrowser(started.url);
12591
+ await announceApproval(ctx, "the sign-in", started.url, started.expiresAt, {
12592
+ noBrowser: options.browser === false
12593
+ });
12427
12594
  return finishLogin(ctx, pending);
12428
12595
  };
12429
12596
  var keyRows = (ctx, keys) => keys.map((key) => [
@@ -12442,6 +12609,9 @@ var keyJson = (ctx, key) => ({
12442
12609
  expiresAt: key.expiresAt,
12443
12610
  written: { file: displayPath(ctx.rt, key.file), name: key.variable }
12444
12611
  });
12612
+ var indented = (table) => table.split(`
12613
+ `).map((line) => ` ${line}`).join(`
12614
+ `);
12445
12615
  var status = async (ctx) => {
12446
12616
  const config = await ctx.config();
12447
12617
  const baseUrl = ctx.baseUrl;
@@ -12472,11 +12642,15 @@ var status = async (ctx) => {
12472
12642
  organisations: signedIn ? config.organisations ?? [] : [],
12473
12643
  apiKeysCreated
12474
12644
  };
12645
+ const defaultOrg = requestedSlug(ctx.opts.org, ctx.rt.env, config);
12475
12646
  const lines = [];
12476
12647
  if (signedIn && config.signIn) {
12477
- lines.push(`Signed in as ${config.signIn.email} (sign-in valid until ${config.signIn.expiresAt.slice(0, 10)}).`, ` API: ${baseUrl}`, ` Default org: ${config.defaultOrg ?? "(none, run `deepdock org select`)"}`);
12478
- if (data.organisations.length > 0)
12479
- lines.push("", "Organisations:", orgTable(ctx, data.organisations));
12648
+ lines.push(`Signed in as ${config.signIn.email} (sign-in valid until ${config.signIn.expiresAt.slice(0, 10)}).`);
12649
+ if (baseUrl !== DEFAULT_BASE_URL)
12650
+ lines.push(ctx.out("yellow", `API: ${baseUrl} (not production)`));
12651
+ if (data.organisations.length > 0) {
12652
+ lines.push("", "Organisations:", indented(orgTable(data.organisations, defaultOrg, ctx.out)), "", defaultOrg ? "Switch the default with `deepdock org select <slug>`." : "No default yet: choose one with `deepdock org select <slug>`.");
12653
+ }
12480
12654
  } else {
12481
12655
  lines.push(config.signIn ? "Your sign-in has expired. Run `deepdock auth login`." : "Not signed in. Run `deepdock auth login`.");
12482
12656
  }
@@ -12527,7 +12701,19 @@ var revokeSignInById = async (ctx, id) => {
12527
12701
  throw error;
12528
12702
  }
12529
12703
  };
12530
- 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) => {
12531
12717
  const config = await ctx.config();
12532
12718
  const signIn = config.signIn;
12533
12719
  const email = signIn?.email ?? null;
@@ -12535,8 +12721,10 @@ var logout = async (ctx) => {
12535
12721
  const revokedKeys = [];
12536
12722
  const failedKeys = [];
12537
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);
12538
12726
  if (usable) {
12539
- for (const entry of [...config.keys]) {
12727
+ for (const entry of keepKeys ? [] : candidates) {
12540
12728
  const outcome = await revokeRecordedKey(ctx, entry);
12541
12729
  if ("error" in outcome)
12542
12730
  failedKeys.push(outcome);
@@ -12548,7 +12736,7 @@ var logout = async (ctx) => {
12548
12736
  signInRevoked = true;
12549
12737
  }
12550
12738
  } else {
12551
- for (const entry of config.keys) {
12739
+ for (const entry of keepKeys ? [] : candidates) {
12552
12740
  failedKeys.push({
12553
12741
  ...factsOf(entry),
12554
12742
  error: { code: "sign_in_needed", message: "Your sign-in is missing or expired." }
@@ -12560,27 +12748,40 @@ var logout = async (ctx) => {
12560
12748
  current.defaultOrg = undefined;
12561
12749
  current.organisations = undefined;
12562
12750
  current.pending = [];
12751
+ if (keepKeys) {
12752
+ for (const entry of current.keys)
12753
+ entry.kept = true;
12754
+ }
12563
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
+ }));
12564
12761
  const lines = [];
12565
12762
  if (!email)
12566
12763
  lines.push("Not signed in.");
12567
12764
  else if (signInRevoked)
12568
- lines.push(`Signed out ${email}. The sign-in is revoked.`);
12765
+ lines.push(`${signedOut} ${email}. The sign-in is revoked.`);
12569
12766
  else if (usable) {
12570
- 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.`);
12571
12768
  } else {
12572
- 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.`);
12573
12770
  }
12574
12771
  if (revokedKeys.length > 0) {
12575
- lines.push("Revoked the API keys this CLI created:");
12772
+ lines.push(`${paint(ctx.out, "success", "Revoked")} the API keys this CLI created:`);
12576
12773
  for (const key of revokedKeys) {
12577
12774
  const where = key.removed.removal === "done" ? ` ${describeRemoval(key.removed)}` : "";
12578
12775
  lines.push(` ${key.org} "${key.name}" ${key.prefix}… ${key.id}${where}`);
12579
12776
  }
12580
12777
  lines.push("Programs still using one of them stop working now.");
12581
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
+ }
12582
12783
  for (const key of failedKeys) {
12583
- 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\`.`);
12584
12785
  }
12585
12786
  const hints = [
12586
12787
  ...failedKeys.length > 0 ? ["Revoke the remaining keys with `deepdock keys revoke <id>`."] : [],
@@ -12591,6 +12792,7 @@ var logout = async (ctx) => {
12591
12792
  revoked: signInRevoked,
12592
12793
  email,
12593
12794
  apiKeysRevoked: revokedKeys,
12795
+ apiKeysKept: keptKeys,
12594
12796
  ...failedKeys.length > 0 ? { apiKeysNotRevoked: failedKeys } : {}
12595
12797
  },
12596
12798
  meta: hints.length > 0 ? { hint: hints.join(" ") } : {},
@@ -12600,38 +12802,10 @@ var logout = async (ctx) => {
12600
12802
  };
12601
12803
  var registerAuth = (program, run) => {
12602
12804
  const auth = program.command("auth").description("Sign in to Deepdock as yourself, see who is signed in, sign out");
12603
- 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)));
12604
12806
  loginCommand.helpInformation = () => AUTH_LOGIN_HELP;
12605
12807
  auth.command("status").description("Show who is signed in").action((_options, cmd) => run(cmd, status));
12606
- auth.command("logout").description("Revoke the sign-in and the API keys this CLI created").action((_options, cmd) => run(cmd, logout));
12607
- };
12608
-
12609
- // src/output/confirm.ts
12610
- import { createInterface } from "node:readline";
12611
- var ask = (rt, prompt) => new Promise((resolve) => {
12612
- const rl = createInterface({ input: rt.stdin.stream, output: undefined, terminal: false });
12613
- rt.stderr.write(prompt);
12614
- let settled = false;
12615
- rl.once("line", (line) => {
12616
- settled = true;
12617
- rl.close();
12618
- resolve(line.trim());
12619
- });
12620
- rl.once("close", () => {
12621
- if (!settled)
12622
- resolve("");
12623
- });
12624
- });
12625
- var confirm = async (rt, question) => {
12626
- const answer = await ask(rt, `${question} [y/N]: `);
12627
- return /^y(es)?$/i.test(answer);
12628
- };
12629
- var pickIndex = async (rt, count, defaultIndex) => {
12630
- const answer = await ask(rt, `Number [${defaultIndex}]: `);
12631
- const chosen = answer === "" ? defaultIndex : Number.parseInt(answer, 10);
12632
- if (!Number.isInteger(chosen) || chosen < 1 || chosen > count)
12633
- return;
12634
- 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)));
12635
12809
  };
12636
12810
 
12637
12811
  // src/commands/devices.ts
@@ -12641,7 +12815,7 @@ var updateNotes = (ctx, devices) => {
12641
12815
  const update = field(device, "updateAvailable");
12642
12816
  if (update) {
12643
12817
  const name = field(device, "label") || field(device, "id");
12644
- ctx.note(`${stripControl(name)}: update available (client ${stripControl(update)}).`);
12818
+ ctx.note(ctx.err("yellow", `${stripControl(name)}: update available (client ${stripControl(update)}).`));
12645
12819
  }
12646
12820
  }
12647
12821
  };
@@ -12676,7 +12850,9 @@ var pair = async (ctx, code, label) => {
12676
12850
  const device = isRecord(response.data) ? response.data : {};
12677
12851
  const id = field(device, "id");
12678
12852
  const name = field(device, "label");
12679
- const lines = [`Paired device ${id}${name ? ` (${stripControl(name)})` : ""}.`];
12853
+ const lines = [
12854
+ `${paint(ctx.out, "success", "Paired")} device ${id}${name ? ` (${stripControl(name)})` : ""}.`
12855
+ ];
12680
12856
  const status = field(device, "status");
12681
12857
  if (status)
12682
12858
  lines.push(` Status: ${status}`);
@@ -12698,7 +12874,7 @@ var rename = async (ctx, id, label) => {
12698
12874
  return {
12699
12875
  data: response.data,
12700
12876
  meta: meta(response.requestId),
12701
- text: `Renamed device ${deviceId} to ${stripControl(label)}.`
12877
+ text: `${paint(ctx.out, "success", "Renamed")} device ${deviceId} to ${stripControl(label)}.`
12702
12878
  };
12703
12879
  };
12704
12880
  var release = async (ctx, id) => {
@@ -12727,7 +12903,7 @@ var release = async (ctx, id) => {
12727
12903
  return {
12728
12904
  data: response.data,
12729
12905
  meta: meta(response.requestId),
12730
- text: `Released device ${deviceId}.`
12906
+ text: `${paint(ctx.out, "success", "Released")} device ${deviceId}.`
12731
12907
  };
12732
12908
  };
12733
12909
  var renderSystems = (ctx, data) => {
@@ -12850,13 +13026,14 @@ var finishKey = async (ctx, pending, warned) => {
12850
13026
  const file = displayPath(ctx.rt, pending.file);
12851
13027
  const days = Math.round((Date.parse(created.expiresAt) - ctx.rt.now().getTime()) / DAY_MS);
12852
13028
  const text = [
12853
- `Created API key "${stripControl(created.name)}".`,
13029
+ `${paint(ctx.out, "success", "Created")} API key "${stripControl(created.name)}".`,
12854
13030
  ` ID: ${created.id}`,
12855
13031
  ` Prefix: ${created.keyPrefix}`,
12856
13032
  ` Expires: ${created.expiresAt.slice(0, 10)} (${days} days)`,
12857
13033
  ` Written: ${file} (${pending.variable})`,
12858
13034
  `The key value was shown to nobody and this CLI keeps no copy; your code reads it from ${file}.`,
12859
- `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.`
12860
13037
  ].join(`
12861
13038
  `);
12862
13039
  return {
@@ -12878,6 +13055,7 @@ var create = async (ctx, name, options) => {
12878
13055
  if (!pending) {
12879
13056
  throw usageError(`There is no pending key request ${options.wait}.`, "Start one with `deepdock keys create ... --no-wait`.");
12880
13057
  }
13058
+ noteWaiting(ctx);
12881
13059
  return finishKey(ctx, pending, false);
12882
13060
  }
12883
13061
  if (name === undefined || name.trim() === "" || name.length > 100 || stripControl(name) !== name) {
@@ -12914,13 +13092,15 @@ var create = async (ctx, name, options) => {
12914
13092
  await upsertPending(ctx, pending);
12915
13093
  if (options.wait === false || !ctx.interactive) {
12916
13094
  return pendingResult(pending, started.url, [
12917
- `Open this link to approve the key: ${started.url}`,
13095
+ `Open this link to approve the key: ${ctx.out("link", started.url)}`,
12918
13096
  `Then run \`deepdock keys create --wait ${started.request}\`.`
12919
13097
  ].join(`
12920
13098
  `));
12921
13099
  }
12922
- 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.`);
12923
- 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
+ });
12924
13104
  return finishKey(ctx, pending, true);
12925
13105
  };
12926
13106
  var revoke = async (ctx, id) => {
@@ -12962,7 +13142,7 @@ Every program using it stops working at once.`;
12962
13142
  const removed = entry ? await settleWrittenLine(ctx, entry, true) : null;
12963
13143
  const apiData = isRecord(response.data) ? response.data : {};
12964
13144
  const label = entry ? `API key "${stripControl(entry.name)}"` : `API key ${keyId}`;
12965
- 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}.`;
12966
13146
  return {
12967
13147
  data: { ...apiData, ...alreadyRevoked ? { alreadyRevoked: true } : {}, removed },
12968
13148
  meta: { requestId: response.requestId },
@@ -12973,7 +13153,7 @@ var registerKeys = (program, run) => {
12973
13153
  const keys = program.command("keys").description("Create, list and revoke your organisation's API keys");
12974
13154
  keys.command("list").description("List the API keys of the organisation").action((_options, cmd) => run(cmd, list2));
12975
13155
  const allowed = Object.keys(EXPIRY_DAYS).join("|");
12976
- 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)));
12977
13157
  createCommand.helpInformation = () => KEYS_CREATE_HELP;
12978
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)));
12979
13159
  };
@@ -12986,12 +13166,12 @@ var list3 = async (ctx) => {
12986
13166
  current.organisations = orgs;
12987
13167
  });
12988
13168
  const defaultOrg = requestedSlug(ctx.opts.org, ctx.rt.env, config);
12989
- const text = orgs.length === 0 ? "You are not a member of an organisation yet." : renderTable(["SLUG", "NAME", "ROLE", "DEFAULT"], orgs.map((org) => [
12990
- org.slug ?? "(no slug)",
12991
- org.name,
12992
- org.role,
12993
- org.slug !== null && org.slug === defaultOrg ? "yes" : ""
12994
- ]), ctx.out);
13169
+ const text = orgs.length === 0 ? "You are not a member of an organisation yet." : [
13170
+ orgTable(orgs, defaultOrg, ctx.out),
13171
+ "",
13172
+ "Switch the default with `deepdock org select <slug>`."
13173
+ ].join(`
13174
+ `);
12995
13175
  return {
12996
13176
  data: response.data,
12997
13177
  meta: response.requestId ? { requestId: response.requestId } : {},
@@ -13018,7 +13198,7 @@ var select = async (ctx, slug) => {
13018
13198
  throw usageError("You are not a member of an organisation yet.");
13019
13199
  ctx.note("Which organisation should be the default?");
13020
13200
  choices.forEach((org, index) => {
13021
- ctx.note(` ${index + 1} ${org.slug.padEnd(18)} ${org.name.padEnd(24)} ${org.role}`);
13201
+ ctx.note(stripControl(` ${index + 1} ${org.slug.padEnd(18)} ${org.name.padEnd(24)} ${roleLabel(org.role)}`));
13022
13202
  });
13023
13203
  const current = Math.max(1, choices.findIndex((org) => org.slug === config.defaultOrg) + 1);
13024
13204
  const index = await pickIndex(ctx.rt, choices.length, current);
@@ -13042,7 +13222,7 @@ var select = async (ctx, slug) => {
13042
13222
  return {
13043
13223
  data: { slug: found.slug, name: found.name, role: found.role },
13044
13224
  meta: response.requestId ? { requestId: response.requestId } : {},
13045
- text: `Default organisation: ${found.name} (${found.slug}).`
13225
+ text: `${paint(ctx.out, "success", "Default organisation:")} ${found.name} (${found.slug}).`
13046
13226
  };
13047
13227
  };
13048
13228
  var registerOrg = (program, run) => {
@@ -13171,10 +13351,10 @@ var registerSystems = (program, run) => {
13171
13351
  systems.command("list").description("List the systems assigned to the organisation").action((_options, cmd) => run(cmd, async (ctx) => {
13172
13352
  const response = await ctx.callInOrg({ method: "GET", path: "/partner/systems" });
13173
13353
  const items = Array.isArray(response.data) ? response.data : [];
13174
- const text = items.length === 0 ? "No systems are assigned to this organisation." : genericTable(items, ctx.out, SYSTEM_COLUMNS);
13354
+ const text = items.length === 0 ? "No systems are assigned to this organisation." : systemsTable(items, ctx.out);
13175
13355
  return { data: response.data, meta: { requestId: response.requestId }, text };
13176
13356
  }));
13177
- systems.command("openapi").description("Print a system's OpenAPI document (redirect to keep it)").argument("<system>", "the system name").action((system, _options, cmd) => run(cmd, async (ctx) => {
13357
+ systems.command("openapi").description("Print a system's OpenAPI document (the document itself when redirected)").argument("<system>", "the system name").action((system, _options, cmd) => run(cmd, async (ctx) => {
13178
13358
  const name = systemArg(system);
13179
13359
  const response = await ctx.callInOrg({
13180
13360
  method: "GET",
@@ -13184,7 +13364,8 @@ var registerSystems = (program, run) => {
13184
13364
  return {
13185
13365
  data: response.data,
13186
13366
  meta: { requestId: response.requestId },
13187
- text: JSON.stringify(document, null, 2)
13367
+ text: JSON.stringify(document, null, 2),
13368
+ bare: true
13188
13369
  };
13189
13370
  }));
13190
13371
  };
@@ -13278,7 +13459,10 @@ var buildProgram = (rt, state) => {
13278
13459
  const meta = { ...ctx.org ? { org: ctx.org } : {}, ...result.meta };
13279
13460
  if (meta.requestId === undefined)
13280
13461
  delete meta.requestId;
13281
- if (ctx.json)
13462
+ if (ctx.json && result.bare && !ctx.opts.json)
13463
+ rt.stdout.write(`${result.text}
13464
+ `);
13465
+ else if (ctx.json)
13282
13466
  rt.stdout.write(toLine(successDoc(result.data, meta)));
13283
13467
  else
13284
13468
  rt.stdout.write(`${result.text}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deepdockai/cli",
3
- "version": "1.0.0",
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",
package/skill/SKILL.md CHANGED
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: deepdock-integration
3
- description: Build, debug or test an integration with Deepdock's partner API, the REST actions under /v1 that read and write records in a customer's on-premise business system (for example Example ERP) through a paired Deepdock device. Use when code calls api.deepdock.ai, handles ddk_ API keys, pairs a device by code, calls a system action, handles error codes such as device_offline, action_paused, idempotency_conflict or outcome_unknown, or runs the deepdock CLI.
3
+ description: Build, debug or test an integration with Deepdock's partner API, the REST actions under /v1 that read and write records in a customer's on-premise business system (for example MEDICAL OFFICE) through a paired Deepdock device. Use when code calls api.deepdock.ai, handles ddk_ API keys, pairs a device by code, calls a system action, handles error codes such as device_offline, action_paused, idempotency_conflict or outcome_unknown, or runs the deepdock CLI.
4
4
  metadata:
5
5
  cli: "@deepdockai/cli"
6
6
  ---
@@ -27,7 +27,7 @@ integration against a demo device. It is not for building or changing actions: D
27
27
  5. Every write needs an `Idempotency-Key`: a new key per intended write, the same key on every retry,
28
28
  and a repeat answers the stored result with `Idempotent-Replayed: true`, writing nothing twice.
29
29
  6. A write either fully applies or not: Deepdock checks its result before confirming, and a write
30
- whose result does not match is undone and the action is paused.
30
+ whose result does not match is not saved and the action is paused.
31
31
  7. A device can be offline and an action can be paused; your code treats both as normal answers.
32
32
  8. Every answer is `{ "data": ... }` or `{ "error": { "code", "message" } }`; `code` is the contract,
33
33
  `message` is for people.
@@ -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.
@@ -19,8 +19,10 @@ curl "$BASE/v1/devices/$DEVICE/systems/example-erp/actions/searchCustomers?lastN
19
19
  { "data": { "items": [ { "customerId": 1001, "lastName": "Müller", "city": "Beispielstadt" } ], "nextCursor": null } }
20
20
  ```
21
21
 
22
- - `nextCursor` is set only when a page came back full; pass it back as `cursor` unchanged. It is
23
- opaque: never parse, build or store it beyond the next request.
22
+ - Pass `nextCursor` back as `cursor` unchanged while it is not `null`, and stop only on `null`,
23
+ never on a short page: a page of long entries (notes) ends before `limit` rows to keep the
24
+ device's answer under 1 MiB and still carries a cursor. It is opaque: never parse, build or store
25
+ it beyond the next request.
24
26
  - Items are the customer's records. Values can be `null` where the schema says so. Dates and other
25
27
  non-numeric values arrive as text in the system's own format (the schema and description say
26
28
  which); numbers as JSON numbers.
@@ -69,6 +71,37 @@ of a customer, the note id of an update. The pattern is always the same:
69
71
 
70
72
  Never invent an id, copy one from another device, or hard-code one from a test run.
71
73
 
74
+ ## Recipe: MEDICAL OFFICE (`medical-office`)
75
+
76
+ The examples above use the placeholder system `example-erp`. For a practice running MEDICAL OFFICE
77
+ the same patterns use real actions (the live OpenAPI document is still the authority for fields):
78
+
79
+ 1. `readUsers` lists the users who can author an entry; pass one id as `authorUserId`.
80
+ 2. `searchPatients?lastName=...` finds a patient: a prefix match with umlauts spelled out
81
+ (`Müller` and `Mueller` match, `Muller` does not), answering `patientId`, `lastName`,
82
+ `firstName`, `birthDate` (`yyyy-mm-dd`, `null` when the system holds no readable date).
83
+ 3. `readCases` lists the patient's cases; a write that needs an open case (`createDiagnosis` and
84
+ the note actions) refuses with `422 no_open_case` when there is none, or takes a `caseId`.
85
+ `readProviders` lists the providers for `providerId` (`422 unknown_provider` otherwise).
86
+ 4. Write with `createDiagnosis` (and the other create actions) under a fresh `Idempotency-Key`,
87
+ then read back.
88
+
89
+ Things the schemas show but are easy to get wrong:
90
+
91
+ - **Ids come back as integers and go in as strings.** A read and a create answer ids as integers
92
+ (a create's new `entryId` or `diagnosisId` too); only an update echoes the key you sent, as the
93
+ string you sent. `patientId: 1001` in a search result goes back as `patientId=1001` in a read's
94
+ query string and as `"patientId": "1001"` in a write body: digits only, no leading zero, at most
95
+ 10 characters.
96
+ - **Nothing is undone through the API.** No action deletes or reverts a record; a write you
97
+ regret stays until someone removes it in MEDICAL OFFICE itself. Write only what you mean to keep.
98
+ - **Mark every test text `TEST-DEEPDOCK-<id>`** (a diagnosis text, a note), on the demo device
99
+ only, and list what you created ([testing.md](testing.md)).
100
+ - Text fields accept MEDICAL OFFICE's Windows-1252 repertoire only; anything else (a tab or line
101
+ break outside a note) answers `422 invalid_input` before a write. Note actions take a `format` of `text` (default, stored as
102
+ typed) or `markdown`.
103
+ - A read action's query string treats `+` as a space: send a literal `+` as `%2B`.
104
+
72
105
  ## Timeouts and latency
73
106
 
74
107
  - Every action call is sent to the customer's PC and run there. Expect a read to take from a few
@@ -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
@@ -81,8 +83,8 @@ A failure is one document on stderr: the API's error object unchanged plus what
81
83
  |---|---|---|
82
84
  | 0 | done | |
83
85
  | 1 | something on the command line to fix | `invalid_usage` (a bad flag, an unknown slug, a question with no terminal and no `--yes`, an env file git does not ignore), `org_required` |
84
- | 2 | the API refused the request; do not repeat it unchanged | `invalid_request`, `not_found`, `device_not_found`, `pairing_code_invalid`, `other_partner_build`, `rate_limited`, `forbidden`, `conflict` and the other refusals |
85
- | 3 | retry later | `device_offline`, `action_paused`, `system_unavailable`, `target_busy`, `temporarily_unavailable`, `internal_error`, `connection_lost` |
86
+ | 2 | the API refused the request; do not repeat it unchanged | `invalid_request`, `not_found`, `device_not_found`, `pairing_code_invalid`, `other_partner_build`, `rate_limited`, `forbidden`, `conflict` and the other refusals (not `access_outdated` and `access_missing`, see exit 3) |
87
+ | 3 | retry later (`access_outdated` and `access_missing`: after the practice repeats "Erneut einrichten" in the Deepdock client) | `device_offline`, `action_paused`, `access_outdated`, `access_missing`, `system_unavailable`, `target_busy`, `temporarily_unavailable`, `internal_error`, `connection_lost` |
86
88
  | 4 | the CLI is too old for the API: run `npx @deepdockai/cli@latest` | `client_outdated` |
87
89
  | 5 | sign-in needed: run `deepdock auth login` | `sign_in_needed`, `approval_denied`, `approval_expired` |
88
90
 
@@ -50,10 +50,12 @@ has a display `label` (may be `null`), a `version` the device reported, a `statu
50
50
  | system `status` | `running` | the customer's program is running on the device | normal |
51
51
  | system `status` | `stopped` | the program is installed but not running | calls answer `system_unavailable`; the site starts the program |
52
52
  | system `status` | `missing` | the program was not found where the device expects it | calls answer `system_unavailable`; contact the site and Deepdock |
53
+ | system `status` | `unknown` | the device could not tell whether the program runs (its probe failed or is unavailable on that PC) | do not read it as stopped; the device decides when a call arrives |
54
+ | system `access` | `ok`, `outdated`, `missing`, `not_set_up`, or `null` | the database access the device last reported (`null`: none known yet) | `outdated` refuses the actions that need the missing right with `access_outdated`, `missing` refuses every action with `access_missing`; the practice repeats "Erneut einrichten" in the Deepdock client, the incident closes by itself once `access` is `ok`, nothing to resume |
53
55
  | action state | `active` | the action can be called | normal |
54
56
  | action state | `paused` | the action is paused with a `reason` and `pausedAt` | calls answer `action_paused`; show the feature as paused, poll `GET .../systems` at a low rate |
55
57
  | pause `reason` | `structure_changed` | the system changed in a way the action depends on (for example a vendor update); Deepdock reviews and resumes | wait; nothing to do on your side |
56
- | pause `reason` | `check_failed` | a write's result did not match and was undone | wait; tell the user the change was not saved |
58
+ | pause `reason` | `check_failed` | a write's result did not match, so nothing was saved | wait; tell the user the change was not saved |
57
59
  | pause `reason` | `system_stopped`, `system_missing` | as the system statuses above; these need no resume and clear when the system is back | as above |
58
60
 
59
61
  Switch on these values; any value you do not know is shown as it is.
@@ -69,8 +71,8 @@ catalogue.
69
71
  - A **read** is `GET` with query parameters and pages.
70
72
  - A **write** is `POST` (creates a record, answers `201`) or `PATCH` (updates a record, answers
71
73
  `200`), with a JSON body of string fields and an `Idempotency-Key` header.
72
- - A write **either fully applies or not**: Deepdock checks the result before confirming, and undoes
73
- the write and pauses the action if the check fails (`409 check_failed`).
74
+ - A write **either fully applies or not**: Deepdock checks the result before confirming, and if the check fails it saves nothing
75
+ and pauses the action (`409 check_failed`).
74
76
  - An action is **active** or **paused**. `GET /v1/devices/{deviceId}/systems` shows each action's
75
77
  state with `reason` and `pausedAt`.
76
78
 
@@ -82,7 +84,7 @@ the action expects:
82
84
  | Reason | What you get | Meaning |
83
85
  |---|---|---|
84
86
  | `structure_changed` | `503 action_paused` | the system changed (for example a vendor update); Deepdock reviews and resumes |
85
- | `check_failed` | that call `409 check_failed`, later calls `503 action_paused` | a write's result did not match and was undone |
87
+ | `check_failed` | that call `409 check_failed`, later calls `503 action_paused` | a write's result did not match, so nothing was saved |
86
88
  | `system_stopped`, `system_missing` | `503 system_unavailable` | the customer's program is stopped or not found on the device |
87
89
 
88
90
  Each pause opens an **incident** (`GET /v1/incidents`). Only Deepdock lifts a pause; staff at the
@@ -7,7 +7,7 @@ Never hard-code an action's fields. Three sources, each authoritative for one th
7
7
  | Which systems and actions does my organisation have? | `deepdock systems list --json` | CLI sign-in |
8
8
  | What does this device run, and is each action active or paused? | `GET /v1/devices/{deviceId}/systems` | organisation |
9
9
  | Which actions can I call on this device's system, with which schemas? | `GET /v1/devices/{deviceId}/systems/{system}/actions` | organisation |
10
- | The exact paths, methods, fields, patterns, enums, lengths, responses | `deepdock systems openapi <system>` or `deepdock actions list <device> <system>` (or `GET /v1/systems/{system}/openapi.json`) | CLI sign-in or API key |
10
+ | The exact paths, methods, fields, patterns, enums, lengths, responses | `deepdock systems openapi <system>` (CLI sign-in) or `GET /v1/systems/{system}/openapi.json` (API key); per device `deepdock actions list <device> <system>` (CLI sign-in) | CLI sign-in or API key |
11
11
 
12
12
  ## The OpenAPI document
13
13
 
@@ -17,7 +17,9 @@ deepdock systems openapi example-erp --org <slug> > openapi/example-erp.json
17
17
  curl "$BASE/v1/systems/example-erp/openapi.json" -H "Authorization: Bearer $DEEPDOCK_API_KEY"
18
18
  ```
19
19
 
20
- OpenAPI 3.1, one document per system, cacheable for 60 seconds. Discover through the CLI; check the
20
+ OpenAPI 3.1, one document per system, cacheable privately for 60 seconds. It is never public:
21
+ `GET /v1/systems/{system}/openapi.json` needs your API key like the rest of `/v1`
22
+ (`401 unauthenticated` without one), and the CLI fetches it with its sign-in instead. Discover through the CLI; check the
21
23
  document into the repository and refresh it with the same command when you touch the integration.
22
24
 
23
25
  What to read off it:
@@ -14,12 +14,14 @@ with the **same** `Idempotency-Key`); **Show** says what the end user should see
14
14
  | Code | Status | Meaning | Retry | Show |
15
15
  |---|---|---|---|---|
16
16
  | `invalid_request` | 400 (405 for a wrong method) | malformed request: bad query, `limit` or `cursor`; missing or malformed `Idempotency-Key`; a body that is not an object of strings; a malformed pairing code or device id; a write called with the other method (`Allow` names the right one) | no; fix the code | nothing; a developer bug |
17
+ | `payload_too_large` | 413 | the request body is over 1 MiB | no; send less | nothing; a developer bug, or shorten the text |
17
18
  | `unauthenticated` | 401 | the key is missing, malformed, for the other environment, unknown, revoked or expired | no | "Deepdock API key refused; check it in the dashboard" to an operator |
18
19
  | `not_found` | 404 | no such route; an update whose id matches no record; a write naming a record that does not exist or belongs to another one | no | "record not found" when it is a record; otherwise a bug |
19
20
  | `rate_limited` | 429 | too many failed pairing attempts with this key (5 per 60 s) | yes, after up to a minute (`Retry-After` when present) | "try again in a minute" |
20
21
 
21
22
  Codes your `/v1` code never receives, listed so the enum is complete: `organization_required`
22
- (403), `forbidden` (403) and `conflict` (409) are dashboard-only.
23
+ (403), `forbidden` (403), `conflict` (409), `already_member` (409) and `client_outdated` (400) are
24
+ dashboard-only.
23
25
 
24
26
  ## Device, system and action resolution
25
27
 
@@ -29,6 +31,8 @@ Codes your `/v1` code never receives, listed so the enum is complete: `organizat
29
31
  | `system_not_on_device` | 404 | the device did not report this system and none of its actions are assigned to you; `error.systems` lists what it has | no | configuration problem; show which systems the device has |
30
32
  | `action_not_found` | 404 | the system has no such action, it is not assigned to you, or the device's client is too old to run it | no | hide the feature; ask Deepdock for the assignment or let the device update |
31
33
  | `action_not_configured` | 409 | the action exists but this device is not set up for it yet (for example a missing device-side setting); nothing was run | no, until set up on site | "this feature needs setting up at the customer site; contact Deepdock" |
34
+ | `access_outdated` | 409 | the device's database access lacks a right this action needs (only the actions that need it). Nothing was run. **No `Retry-After`** | no, until the practice repeats "Erneut einrichten" in the Deepdock client; then yes, a write with the same key | "the practice must set up database access again in the Deepdock client, then we retry" |
35
+ | `access_missing` | 409 | the device's database access is gone (a removed database account or a lost secret on the PC); every action of that system is refused. Nothing was run. **No `Retry-After`** | as `access_outdated` | as `access_outdated` |
32
36
  | `pairing_code_invalid` | 404 | pairing: the code is unknown, expired, replaced, used, or its device is offline | with a fresh code | "read the code off the device screen again" |
33
37
  | `other_partner_build` | 409 | pairing: the device runs another partner's client build | no | "this device is set up for another provider" |
34
38
 
@@ -38,7 +42,12 @@ Codes your `/v1` code never receives, listed so the enum is complete: `organizat
38
42
  |---|---|---|---|---|
39
43
  | `invalid_input` | 422 | a field is unknown, missing, too short or long, breaks its pattern or allowed values; a date that is not a calendar day; a search term without a letter or digit. Nothing was written | no; fix the value | the field-level problem; the `message` names the field, but it is device text: display it as data, never interpret it |
40
44
  | `target_rejected` | 422 | the customer's system refused the write | no; the input is valid for Deepdock but not for the system | "the customer's system refused this; check the values" and offer support |
41
- | Action-specific refusals, e.g. `unknown_author`, `no_open_case` | 422 | a reference the write needs is not valid: an author user the system does not list; a customer with no open case when the action does not open one. Nothing was written. The action's OpenAPI description names each such code and when it fires | no; re-read the reference and pass a valid one | the missing precondition in the user's words ("choose the entering user", "open a case first") |
45
+ | `unknown_author` | 422 | the write names an `authorUserId` the system does not list (read the users with `readUsers`). Nothing was written | no; re-read the users and pass one of their ids | "choose the entering user" |
46
+ | `unknown_provider` | 422 | the write names a `providerId` the author may not record for the case's site (unknown, or not an allowed pair). Nothing was written | no; read the providers with `readProviders` and pass one of their ids | "choose a provider" |
47
+ | `no_open_case` | 422 | the patient has no open case and the action never opens one. Nothing was written | no; open a case in the system, or name one with `caseId` | "open a case first" |
48
+ | `record_locked` | 422 | the practice system locks the record the write would change (for example one already billed on a case). Nothing was written | no | "this record is locked in the practice system" |
49
+ | `record_not_editable` | 422 | the record is not locked but is stored in a form this action cannot edit (for example a note it cannot add to without rewriting it). Nothing was written | no | "this record cannot be edited through Deepdock" |
50
+ | Other action-specific refusals | 422 | the action's OpenAPI description names each such code and when it fires. Nothing was written | no; fix the reference or input | the missing precondition in the user's words |
42
51
 
43
52
  ## Device and system availability
44
53
 
@@ -54,7 +63,7 @@ Codes your `/v1` code never receives, listed so the enum is complete: `organizat
54
63
 
55
64
  | Code | Status | Meaning | Retry | Show |
56
65
  |---|---|---|---|---|
57
- | `check_failed` | 409 | the write was undone because its result did not match what the action expects; **the action is now paused** and an incident is open | no; the next calls answer `action_paused` | "the change was not saved; Deepdock is checking the customer system" |
66
+ | `check_failed` | 409 | the write's result did not match what the action expects, so nothing was saved; **the action is now paused** and an incident is open | no; the next calls answer `action_paused` | "the change was not saved; Deepdock is checking the customer system" |
58
67
  | `idempotency_conflict` | 409 | the key was used with a different input, its write is still running, or its outcome is unknown. One code, three causes; `message` must not be parsed to tell them apart | retry with the **same key and the same input** a bounded number of times with backoff (a running write finishes within seconds); if it still conflicts, handle it like `outcome_unknown`: read back, then decide | "a previous attempt is still in progress" |
59
68
  | `outcome_unknown` | 500 | the system did not confirm the write; it may or may not have landed. The key stays blocked | **no.** Read the record back (a search) to see whether it landed before writing again under a new key; involve a human when the read cannot tell | "we could not confirm the change; please check in the customer system before trying again" |
60
69
  | `internal_error` | 500 | something failed inside Deepdock; `error.requestId` is set | once, later; a write with the same key | "something went wrong"; log `requestId` for Deepdock support |
@@ -62,7 +71,7 @@ Codes your `/v1` code never receives, listed so the enum is complete: `organizat
62
71
  ## A reaction table for code
63
72
 
64
73
  ```ts
65
- type Reaction = 'fix-code' | 'fix-input' | 'retry-same-key' | 'retry-later' | 'wait-paused' | 'check-then-decide' | 'stop'
74
+ type Reaction = 'fix-code' | 'fix-input' | 'retry-same-key' | 'retry-later' | 'wait-paused' | 'wait-setup' | 'check-then-decide' | 'stop'
66
75
 
67
76
  const REACTION: Record<string, Reaction> = {
68
77
  invalid_request: 'fix-code',
@@ -79,6 +88,12 @@ const REACTION: Record<string, Reaction> = {
79
88
  target_rejected: 'fix-input',
80
89
  unknown_author: 'fix-input',
81
90
  no_open_case: 'fix-input',
91
+ unknown_provider: 'fix-input',
92
+ record_locked: 'stop',
93
+ record_not_editable: 'stop',
94
+ payload_too_large: 'fix-code',
95
+ access_outdated: 'wait-setup',
96
+ access_missing: 'wait-setup',
82
97
  device_offline: 'retry-same-key',
83
98
  action_paused: 'wait-paused',
84
99
  system_unavailable: 'retry-later',
@@ -92,8 +107,7 @@ const REACTION: Record<string, Reaction> = {
92
107
  const reactionFor = (code: string): Reaction => REACTION[code] ?? 'stop'
93
108
  ```
94
109
 
95
- Action-specific 422 codes (`unknown_author` and `no_open_case` are two; any the OpenAPI description
96
- lists) map to `fix-input`. Add them from the document, not from memory. `retry-same-key` means the
110
+ Action-specific 422 codes beyond the ones above (any the OpenAPI description lists) map to `fix-input`. Add them from the document, not from memory. `wait-setup` means a person at the practice must repeat the database access setup ("Erneut einrichten") in the Deepdock client first; there is no `Retry-After`, so do not loop. Check `access` on the system in `GET .../systems` (`ok` again), then retry; a write keeps its key. `retry-same-key` means the
97
111
  pending write stays persisted and is retried from that state with backoff, also across process
98
112
  restarts; a device can be offline for hours, so the retry loop lives in a job or queue, not in the
99
113
  request that started the write.
@@ -13,7 +13,7 @@ refused with `400 invalid_request`.
13
13
  - Keys are remembered **per device**. The same key on another device is a different write.
14
14
  - A successful key is remembered for at least 7 days.
15
15
  - A write that was refused before anything was written (`invalid_input`, `target_rejected`, an
16
- action-specific 422 such as `unknown_author`, `not_found`) or undone (`check_failed`) frees its
16
+ action-specific 422 such as `unknown_author`, `not_found`) or not saved (`check_failed`) frees its
17
17
  key: you may retry with the same key. After changing the input, use a new key; the old one may
18
18
  still be remembered with the old input. `idempotency_conflict` does **not** free a key.
19
19
 
@@ -59,7 +59,7 @@ create, search by the value you wrote; for an update, read the record by id) and
59
59
 
60
60
  ### `409 check_failed`
61
61
 
62
- The write was undone because its result did not match, and the action is now paused. Do not retry;
62
+ The write's result did not match, so nothing was saved, and the action is now paused. Do not retry;
63
63
  the next calls answer `503 action_paused` until Deepdock resumes it. Tell the user the change was not
64
64
  saved.
65
65
 
@@ -27,7 +27,7 @@ gets written to. These rules are not optional.
27
27
 
28
28
  ## Cleanup
29
29
 
30
- No action deletes anything. So:
30
+ No action deletes anything, and nothing is undone through the API. So:
31
31
 
32
32
  1. Every test run prints what it created: record id, device, time, and how many change-log entries
33
33
  the updates added. The CLI runs no action, so your own test prints this.
@@ -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