@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 +20 -0
- package/README.md +24 -10
- package/dist/deepdock.js +283 -99
- package/package.json +1 -1
- package/skill/SKILL.md +2 -2
- package/skill/references/auth-and-keys.md +3 -3
- package/skill/references/calling-actions.md +35 -2
- package/skill/references/cli.md +8 -6
- package/skill/references/concepts.md +6 -4
- package/skill/references/discovery.md +4 -2
- package/skill/references/errors.md +20 -6
- package/skill/references/idempotency.md +2 -2
- package/skill/references/testing.md +3 -3
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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`
|
|
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;
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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) =>
|
|
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.
|
|
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.
|
|
11361
|
-
|
|
11442
|
+
child.once("error", () => resolve(false));
|
|
11443
|
+
child.once("spawn", () => {
|
|
11444
|
+
child.unref();
|
|
11445
|
+
resolve(true);
|
|
11362
11446
|
});
|
|
11363
|
-
|
|
11364
|
-
|
|
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 {
|
|
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
|
|
11835
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
12326
|
-
ctx.note(
|
|
12327
|
-
|
|
12328
|
-
|
|
12329
|
-
|
|
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 = [
|
|
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(
|
|
12384
|
-
|
|
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
|
-
|
|
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)})
|
|
12478
|
-
if (
|
|
12479
|
-
lines.push("",
|
|
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
|
|
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 [
|
|
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
|
|
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(
|
|
12765
|
+
lines.push(`${signedOut} ${email}. The sign-in is revoked.`);
|
|
12569
12766
|
else if (usable) {
|
|
12570
|
-
lines.push(
|
|
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(
|
|
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(
|
|
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((
|
|
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 = [
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
`
|
|
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,
|
|
12923
|
-
|
|
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" ?
|
|
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." :
|
|
12990
|
-
|
|
12991
|
-
|
|
12992
|
-
org
|
|
12993
|
-
|
|
12994
|
-
|
|
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:
|
|
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." :
|
|
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 (
|
|
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
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
|
|
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
|
|
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
|
|
46
|
-
|
|
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`
|
|
23
|
-
|
|
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
|
package/skill/references/cli.md
CHANGED
|
@@ -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`;
|
|
36
|
-
lists
|
|
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
|
-
|
|
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
|
|
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
|
|
73
|
-
|
|
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
|
|
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>`
|
|
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.
|
|
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)
|
|
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
|
-
|
|
|
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
|
|
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 (`
|
|
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
|
|
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
|
|
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
|
|
64
|
-
|
|
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
|