@deepdockai/cli 1.0.2 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/README.md +9 -5
- package/dist/deepdock.js +179 -75
- package/package.json +1 -1
- package/skill/references/auth-and-keys.md +3 -3
- package/skill/references/cli.md +6 -4
- package/skill/references/testing.md +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.3
|
|
4
|
+
|
|
5
|
+
- `auth logout --keep-keys` revokes only the sign-in. On a terminal, plain `auth logout` lists the API keys and asks before revoking them (default: keep); with `--json`, `--yes` or no terminal it still revokes them. `keys create` says so.
|
|
6
|
+
- `keys create` no longer advises revoking the key "when you ship"; it prints the expiry and how to revoke.
|
|
7
|
+
- With exactly one organisation it is used automatically (and named), and `auth login` selects it. With several and none chosen, `org_required` stays.
|
|
8
|
+
- The approval page opens in your browser on a terminal (not in CI, without a display, with `--no-browser` or `DEEPDOCK_NO_BROWSER=1`). The line says "Opening the approval page in your browser" when it does.
|
|
9
|
+
- `Organisation:` is printed once per command, not again after the wait.
|
|
10
|
+
- Output colours by role: warning and error labels, success verbs, bold organisation and approval prompt, dim waiting line.
|
|
11
|
+
- The approval link is blue and underlined, and clickable in terminals that support links. Plain text when output is piped, under `--json`, or with `NO_COLOR`.
|
|
12
|
+
|
|
3
13
|
## 1.0.2
|
|
4
14
|
|
|
5
15
|
- The install commands no longer pin a major version: `npx @deepdockai/cli auth login`, `npm install -g @deepdockai/cli`.
|
package/README.md
CHANGED
|
@@ -30,14 +30,16 @@ holding only invented, marked data. While unpaired, the client shows a pairing c
|
|
|
30
30
|
|
|
31
31
|
## Quick start
|
|
32
32
|
|
|
33
|
-
Sign in. The CLI prints
|
|
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,7 +93,7 @@ deepdock skill
|
|
|
91
93
|
| ----------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
92
94
|
| `auth login` | sign in as yourself, approved in the browser; the sign-in lasts 90 days |
|
|
93
95
|
| `auth status` | who is signed in, the organisations, and the keys this CLI created (id, name, prefix, expiry, file; never the value); exits 5 when not signed in, like `gh auth status` |
|
|
94
|
-
| `auth logout`
|
|
96
|
+
| `auth logout [--keep-keys]` | revoke the sign-in and every key this CLI created (`--keep-keys`: only the sign-in) |
|
|
95
97
|
| `org list`, `org select [slug]` | your organisations; set the default one |
|
|
96
98
|
| `systems list` | the systems and actions assigned to your organisation |
|
|
97
99
|
| `systems openapi <system>` | a system's OpenAPI document on stdout, bare when redirected; save it to a file |
|
|
@@ -110,7 +112,8 @@ and `keys revoke` without a question, `-h, --help` everywhere and `-v, --version
|
|
|
110
112
|
|
|
111
113
|
Two things are never guessed. A device is always named by its full id: no default device, no
|
|
112
114
|
lookup by label. The organisation comes from `--org`, then `DEEPDOCK_ORG`, then the default
|
|
113
|
-
chosen with `deepdock org select
|
|
115
|
+
chosen with `deepdock org select`. With exactly one organisation it is used without asking (and
|
|
116
|
+
named on stderr); with several and none chosen the command stops and lists them.
|
|
114
117
|
|
|
115
118
|
## The approval flow
|
|
116
119
|
|
|
@@ -222,7 +225,8 @@ macOS and Linux need nothing special. On Windows:
|
|
|
222
225
|
- **Keys expire and can be revoked.** A key reaches every device of the organisation until it
|
|
223
226
|
expires (at most 90 days from the CLI) or is revoked. `deepdock keys revoke <id>` revokes it and
|
|
224
227
|
removes the line the CLI wrote; `deepdock auth logout` revokes the sign-in and every key this
|
|
225
|
-
CLI created
|
|
228
|
+
CLI created, unless you pass `--keep-keys`. On a terminal it lists the keys and asks first
|
|
229
|
+
(default: keep); with `--json` or without a terminal it revokes them unless `--keep-keys` is passed. In `--json`, `data.removed` says what happened to the line: `null` for a key this
|
|
226
230
|
CLI never wrote, otherwise `{ file, name, removal }` with `removal` `done`, `absent` (the line
|
|
227
231
|
was already gone or holds another value) or `failed` (with a `reason`). If the file is held open
|
|
228
232
|
by another program (a dev server watching it, on Windows), the key is still revoked and the
|
package/dist/deepdock.js
CHANGED
|
@@ -11323,6 +11323,18 @@ var parseAnswer = async (name, value) => {
|
|
|
11323
11323
|
return result;
|
|
11324
11324
|
};
|
|
11325
11325
|
|
|
11326
|
+
// src/approval/browser.ts
|
|
11327
|
+
var isSet = (value) => value !== undefined && !["", "0", "false"].includes(value.toLowerCase());
|
|
11328
|
+
var browserAllowed = (rt, noBrowser) => {
|
|
11329
|
+
if (noBrowser || isSet(rt.env.DEEPDOCK_NO_BROWSER) || isSet(rt.env.CI))
|
|
11330
|
+
return false;
|
|
11331
|
+
if (isSet(rt.env.SSH_CONNECTION) && rt.platform !== "linux")
|
|
11332
|
+
return false;
|
|
11333
|
+
if (rt.platform === "linux" && !rt.env.DISPLAY && !rt.env.WAYLAND_DISPLAY)
|
|
11334
|
+
return false;
|
|
11335
|
+
return true;
|
|
11336
|
+
};
|
|
11337
|
+
|
|
11326
11338
|
// src/approval/poll.ts
|
|
11327
11339
|
var MAX_CONSECUTIVE_FAILURES = 5;
|
|
11328
11340
|
var EXPIRY_GRACE_MS = 15000;
|
|
@@ -11412,7 +11424,7 @@ import { spawn } from "node:child_process";
|
|
|
11412
11424
|
import { homedir, hostname } from "node:os";
|
|
11413
11425
|
|
|
11414
11426
|
// src/version.ts
|
|
11415
|
-
var VERSION = "1.0.
|
|
11427
|
+
var VERSION = "1.0.3";
|
|
11416
11428
|
|
|
11417
11429
|
// src/runtime.ts
|
|
11418
11430
|
var DEFAULT_BASE_URL = "https://api.deepdock.ai";
|
|
@@ -11423,16 +11435,19 @@ var browserCommand = (platform, url) => {
|
|
|
11423
11435
|
return { command: "rundll32", args: ["url.dll,FileProtocolHandler", url] };
|
|
11424
11436
|
return { command: "xdg-open", args: [url] };
|
|
11425
11437
|
};
|
|
11426
|
-
var openInBrowser = (url) => {
|
|
11438
|
+
var openInBrowser = (url) => new Promise((resolve) => {
|
|
11427
11439
|
const { command, args } = browserCommand(process.platform, url);
|
|
11428
11440
|
try {
|
|
11429
11441
|
const child = spawn(command, args, { stdio: "ignore", detached: true });
|
|
11430
|
-
child.
|
|
11431
|
-
|
|
11442
|
+
child.once("error", () => resolve(false));
|
|
11443
|
+
child.once("spawn", () => {
|
|
11444
|
+
child.unref();
|
|
11445
|
+
resolve(true);
|
|
11432
11446
|
});
|
|
11433
|
-
|
|
11434
|
-
|
|
11435
|
-
}
|
|
11447
|
+
} catch {
|
|
11448
|
+
resolve(false);
|
|
11449
|
+
}
|
|
11450
|
+
});
|
|
11436
11451
|
var defaultRuntime = () => {
|
|
11437
11452
|
const interrupt = new AbortController;
|
|
11438
11453
|
process.once("SIGINT", () => interrupt.abort());
|
|
@@ -11638,6 +11653,9 @@ var requestedSlug = (flag, env, config) => flag || env.DEEPDOCK_ORG || config.de
|
|
|
11638
11653
|
var resolveOrg = (requested, orgs, fromStoredDefault = false) => {
|
|
11639
11654
|
const choices = selectableOrgs(orgs);
|
|
11640
11655
|
const slugs = choices.map((org) => org.slug);
|
|
11656
|
+
const [only] = choices;
|
|
11657
|
+
if (!requested && only && choices.length === 1)
|
|
11658
|
+
return { ...only, onlyOne: true };
|
|
11641
11659
|
if (!requested) {
|
|
11642
11660
|
throw new CliError({
|
|
11643
11661
|
code: "org_required",
|
|
@@ -11747,7 +11765,16 @@ var pickKey = (value) => {
|
|
|
11747
11765
|
const { org, id, name, prefix, expiresAt, file, variable } = entry;
|
|
11748
11766
|
if (!org || !id || !name || !prefix || !expiresAt || !file || !variable)
|
|
11749
11767
|
return;
|
|
11750
|
-
return {
|
|
11768
|
+
return {
|
|
11769
|
+
org,
|
|
11770
|
+
id,
|
|
11771
|
+
name,
|
|
11772
|
+
prefix,
|
|
11773
|
+
expiresAt,
|
|
11774
|
+
file,
|
|
11775
|
+
variable,
|
|
11776
|
+
...value.kept === true ? { kept: true } : {}
|
|
11777
|
+
};
|
|
11751
11778
|
};
|
|
11752
11779
|
var pickPending = (value) => {
|
|
11753
11780
|
if (!isRecord3(value))
|
|
@@ -11900,11 +11927,29 @@ var updateConfig = (rt, change) => withLock(rt, async () => {
|
|
|
11900
11927
|
|
|
11901
11928
|
// src/output/styles.ts
|
|
11902
11929
|
import { styleText } from "node:util";
|
|
11930
|
+
var OSC = "\x1B]";
|
|
11931
|
+
var ST = "\x1B\\";
|
|
11932
|
+
var ROLES = {
|
|
11933
|
+
warn: "yellow",
|
|
11934
|
+
error: "red",
|
|
11935
|
+
success: "green",
|
|
11936
|
+
emphasis: "bold",
|
|
11937
|
+
quiet: "dim",
|
|
11938
|
+
link: "link"
|
|
11939
|
+
};
|
|
11903
11940
|
var makeStyler = (rt, stream) => {
|
|
11904
11941
|
const forced = rt.env.FORCE_COLOR !== undefined && !["", "0"].includes(rt.env.FORCE_COLOR);
|
|
11905
11942
|
const enabled = (stream.isTTY || forced) && !rt.env.NO_COLOR && rt.env.TERM !== "dumb";
|
|
11906
|
-
return (name, text) =>
|
|
11943
|
+
return (name, text) => {
|
|
11944
|
+
if (!enabled)
|
|
11945
|
+
return text;
|
|
11946
|
+
if (name !== "link")
|
|
11947
|
+
return styleText(name, text, { validateStream: false });
|
|
11948
|
+
const styled = styleText(["blue", "underline"], text, { validateStream: false });
|
|
11949
|
+
return `${OSC}8;;${text}${ST}${styled}${OSC}8;;${ST}`;
|
|
11950
|
+
};
|
|
11907
11951
|
};
|
|
11952
|
+
var paint = (style, role, text) => style(ROLES[role], text);
|
|
11908
11953
|
|
|
11909
11954
|
// src/output/terminal.ts
|
|
11910
11955
|
var isInteractive = (rt, json) => rt.stdin.isTTY && rt.stdout.isTTY && !json;
|
|
@@ -11926,6 +11971,7 @@ var createContext = (rt, opts) => {
|
|
|
11926
11971
|
const baseUrl = baseUrlFor(rt);
|
|
11927
11972
|
const api = createApi(rt);
|
|
11928
11973
|
let loaded;
|
|
11974
|
+
let announcedOrg = false;
|
|
11929
11975
|
const config = async () => {
|
|
11930
11976
|
loaded ??= await loadConfig(rt);
|
|
11931
11977
|
return loaded;
|
|
@@ -11946,7 +11992,8 @@ var createContext = (rt, opts) => {
|
|
|
11946
11992
|
return loaded;
|
|
11947
11993
|
},
|
|
11948
11994
|
note: (line) => {
|
|
11949
|
-
|
|
11995
|
+
const warning = "Warning:";
|
|
11996
|
+
rt.stderr.write(`${line.startsWith(warning) ? `${paint(ctx.err, "warn", warning)}${line.slice(warning.length)}` : line}
|
|
11950
11997
|
`);
|
|
11951
11998
|
},
|
|
11952
11999
|
signInSecret: async () => {
|
|
@@ -11978,7 +12025,10 @@ var createContext = (rt, opts) => {
|
|
|
11978
12025
|
}
|
|
11979
12026
|
const resolved = resolveOrg(requested, orgs, fromStoredDefault);
|
|
11980
12027
|
ctx.org = { slug: resolved.slug, name: resolved.name };
|
|
11981
|
-
|
|
12028
|
+
if (!announcedOrg) {
|
|
12029
|
+
announcedOrg = true;
|
|
12030
|
+
ctx.note(resolved.onlyOne ? `${paint(ctx.err, "quiet", "Using organisation")} ${paint(ctx.err, "emphasis", resolved.name)} ${paint(ctx.err, "quiet", `(${resolved.slug}).`)}` : `${paint(ctx.err, "quiet", "Organisation:")} ${paint(ctx.err, "emphasis", resolved.name)} ${paint(ctx.err, "quiet", `(${resolved.slug})`)}`);
|
|
12031
|
+
}
|
|
11982
12032
|
return resolved;
|
|
11983
12033
|
},
|
|
11984
12034
|
callInOrg: async (req) => {
|
|
@@ -12044,12 +12094,14 @@ an action; actions run from your code with an API key (\`deepdock keys create\`)
|
|
|
12044
12094
|
Usage
|
|
12045
12095
|
deepdock auth login [flags]
|
|
12046
12096
|
|
|
12047
|
-
The link is printed either way
|
|
12097
|
+
The link is printed either way. On a terminal the CLI also opens it in your browser, unless
|
|
12098
|
+
CI is set, there is no display, DEEPDOCK_NO_BROWSER=1 is set or you pass --no-browser.
|
|
12048
12099
|
|
|
12049
12100
|
Flags
|
|
12050
12101
|
--no-wait Print the request and exit; finish it later with --wait (the default
|
|
12051
12102
|
when there is no terminal)
|
|
12052
12103
|
--wait <request> Finish a request started with --no-wait
|
|
12104
|
+
--no-browser Do not open the link in a browser
|
|
12053
12105
|
|
|
12054
12106
|
Examples
|
|
12055
12107
|
$ deepdock auth login
|
|
@@ -12057,7 +12109,7 @@ Examples
|
|
|
12057
12109
|
$ deepdock auth login --wait <request>
|
|
12058
12110
|
|
|
12059
12111
|
More
|
|
12060
|
-
\`deepdock auth status\` shows who is signed in (exit 5 when nobody is). \`deepdock auth logout\` revokes the sign-in.
|
|
12112
|
+
\`deepdock auth status\` shows who is signed in (exit 5 when nobody is). \`deepdock auth logout\` revokes the sign-in and, unless you pass --keep-keys, the API keys this CLI created.
|
|
12061
12113
|
`;
|
|
12062
12114
|
var KEYS_CREATE_HELP = `Create an API key for your organisation (admin).
|
|
12063
12115
|
|
|
@@ -12066,7 +12118,8 @@ key once, writes it into the env file named by --write-env, and keeps no copy. T
|
|
|
12066
12118
|
never printed and the CLI never uses it; your code reads it from that file.
|
|
12067
12119
|
|
|
12068
12120
|
A key reaches every device of the organisation until it expires or is revoked. Keep the
|
|
12069
|
-
expiry short
|
|
12121
|
+
expiry short, and revoke a key you no longer need: \`deepdock keys revoke <id>\`. \`deepdock auth logout\`
|
|
12122
|
+
revokes the keys this CLI created unless you pass --keep-keys.
|
|
12070
12123
|
|
|
12071
12124
|
Usage
|
|
12072
12125
|
deepdock keys create <name> --expires <duration> --write-env <file> [flags]
|
|
@@ -12081,6 +12134,7 @@ Flags
|
|
|
12081
12134
|
the CLI warns
|
|
12082
12135
|
--no-wait Print the request and exit; finish it with --wait
|
|
12083
12136
|
--wait <request> Finish a request started with --no-wait
|
|
12137
|
+
--no-browser Do not open the link in a browser (also DEEPDOCK_NO_BROWSER=1)
|
|
12084
12138
|
|
|
12085
12139
|
Examples
|
|
12086
12140
|
$ deepdock keys create "myapp dev" --expires 30d --write-env .env.local
|
|
@@ -12153,6 +12207,34 @@ var formatHelp = (command) => {
|
|
|
12153
12207
|
`;
|
|
12154
12208
|
};
|
|
12155
12209
|
|
|
12210
|
+
// src/output/confirm.ts
|
|
12211
|
+
import { createInterface } from "node:readline";
|
|
12212
|
+
var ask = (rt, prompt) => new Promise((resolve) => {
|
|
12213
|
+
const rl = createInterface({ input: rt.stdin.stream, output: undefined, terminal: false });
|
|
12214
|
+
rt.stderr.write(prompt);
|
|
12215
|
+
let settled = false;
|
|
12216
|
+
rl.once("line", (line) => {
|
|
12217
|
+
settled = true;
|
|
12218
|
+
rl.close();
|
|
12219
|
+
resolve(line.trim());
|
|
12220
|
+
});
|
|
12221
|
+
rl.once("close", () => {
|
|
12222
|
+
if (!settled)
|
|
12223
|
+
resolve("");
|
|
12224
|
+
});
|
|
12225
|
+
});
|
|
12226
|
+
var confirm = async (rt, question) => {
|
|
12227
|
+
const answer = await ask(rt, `${question} [y/N]: `);
|
|
12228
|
+
return /^y(es)?$/i.test(answer);
|
|
12229
|
+
};
|
|
12230
|
+
var pickIndex = async (rt, count, defaultIndex) => {
|
|
12231
|
+
const answer = await ask(rt, `Number [${defaultIndex}]: `);
|
|
12232
|
+
const chosen = answer === "" ? defaultIndex : Number.parseInt(answer, 10);
|
|
12233
|
+
if (!Number.isInteger(chosen) || chosen < 1 || chosen > count)
|
|
12234
|
+
return;
|
|
12235
|
+
return chosen;
|
|
12236
|
+
};
|
|
12237
|
+
|
|
12156
12238
|
// src/writeEnv/ledger.ts
|
|
12157
12239
|
import { relative, sep } from "node:path";
|
|
12158
12240
|
var addKey = (config, entry) => {
|
|
@@ -12393,11 +12475,19 @@ var settleWrittenLine = async (ctx, entry, keepEntryOnFailure) => {
|
|
|
12393
12475
|
|
|
12394
12476
|
// src/commands/auth.ts
|
|
12395
12477
|
var minutesLeft = (ctx, expiresAt) => Math.max(1, Math.ceil((Date.parse(expiresAt) - ctx.rt.now().getTime()) / 60000));
|
|
12396
|
-
var
|
|
12397
|
-
ctx.note(
|
|
12398
|
-
|
|
12399
|
-
|
|
12400
|
-
|
|
12478
|
+
var noteWaiting = (ctx) => {
|
|
12479
|
+
ctx.note(paint(ctx.err, "quiet", "Waiting for approval in the browser."));
|
|
12480
|
+
};
|
|
12481
|
+
var announceApproval = async (ctx, what, url, expiresAt, options) => {
|
|
12482
|
+
const open = browserAllowed(ctx.rt, options.noBrowser);
|
|
12483
|
+
const minutes = minutesLeft(ctx, expiresAt);
|
|
12484
|
+
ctx.note(open ? `Opening the approval page in your browser (it expires in ${minutes} minutes):` : `Open this link to approve ${what} (it expires in ${minutes} minutes):`);
|
|
12485
|
+
ctx.note(` ${ctx.err("link", url)}`);
|
|
12486
|
+
ctx.note(options.extra ?? "The page shows this computer's name and the CLI version: check them before you approve.");
|
|
12487
|
+
if (open) {
|
|
12488
|
+
ctx.note(await ctx.rt.openBrowser(url) ? "Opened in your browser." : "Could not open a browser; use the link above.");
|
|
12489
|
+
}
|
|
12490
|
+
noteWaiting(ctx);
|
|
12401
12491
|
};
|
|
12402
12492
|
var pendingResult = (pending, url, text) => ({
|
|
12403
12493
|
data: { pending: { request: pending.request, url, expiresAt: pending.expiresAt } },
|
|
@@ -12441,14 +12531,22 @@ var finishLogin = async (ctx, pending) => {
|
|
|
12441
12531
|
current.organisations = answer.organisations;
|
|
12442
12532
|
if (current.defaultOrg && !slugs.includes(current.defaultOrg))
|
|
12443
12533
|
current.defaultOrg = undefined;
|
|
12534
|
+
if (!current.defaultOrg && slugs.length === 1)
|
|
12535
|
+
current.defaultOrg = slugs[0];
|
|
12444
12536
|
current.pending = current.pending.filter((entry) => entry.request !== pending.request);
|
|
12445
12537
|
});
|
|
12446
|
-
const lines = [
|
|
12538
|
+
const lines = [`${paint(ctx.out, "success", "Signed in")} as ${answer.user.email}.`, ""];
|
|
12447
12539
|
if (answer.organisations.length === 0) {
|
|
12448
12540
|
lines.push("You are not a member of an organisation yet.");
|
|
12449
12541
|
} else {
|
|
12450
12542
|
lines.push("Your organisations:", indented(orgTable(answer.organisations, config.defaultOrg, ctx.out)), "");
|
|
12451
|
-
|
|
12543
|
+
const only = selectableOrgs(answer.organisations);
|
|
12544
|
+
const [sole] = only;
|
|
12545
|
+
if (sole && only.length === 1 && config.defaultOrg === sole.slug) {
|
|
12546
|
+
lines.push(`${paint(ctx.out, "success", "Selected")} ${sole.name} (${sole.slug}) as your organisation.`);
|
|
12547
|
+
} else {
|
|
12548
|
+
lines.push("Choose one with `deepdock org select <slug>` or pass --org to each command.");
|
|
12549
|
+
}
|
|
12452
12550
|
}
|
|
12453
12551
|
return {
|
|
12454
12552
|
data: {
|
|
@@ -12471,7 +12569,7 @@ var login = async (ctx, options) => {
|
|
|
12471
12569
|
}
|
|
12472
12570
|
assertSameBaseUrl(pending.baseUrl, ctx.baseUrl, "request");
|
|
12473
12571
|
assertNotExpired(ctx.rt, "login", pending.expiresAt);
|
|
12474
|
-
ctx
|
|
12572
|
+
noteWaiting(ctx);
|
|
12475
12573
|
return finishLogin(ctx, pending);
|
|
12476
12574
|
}
|
|
12477
12575
|
const started = await startLogin(ctx.api, ctx.rt);
|
|
@@ -12485,13 +12583,14 @@ var login = async (ctx, options) => {
|
|
|
12485
12583
|
await upsertPending(ctx, pending);
|
|
12486
12584
|
if (options.wait === false || !ctx.interactive) {
|
|
12487
12585
|
return pendingResult(pending, started.url, [
|
|
12488
|
-
`Open this link to approve the sign-in: ${started.url}`,
|
|
12586
|
+
`Open this link to approve the sign-in: ${ctx.out("link", started.url)}`,
|
|
12489
12587
|
`Then run \`deepdock auth login --wait ${started.request}\`.`
|
|
12490
12588
|
].join(`
|
|
12491
12589
|
`));
|
|
12492
12590
|
}
|
|
12493
|
-
announceApproval(ctx, "the sign-in", started.url, started.expiresAt
|
|
12494
|
-
|
|
12591
|
+
await announceApproval(ctx, "the sign-in", started.url, started.expiresAt, {
|
|
12592
|
+
noBrowser: options.browser === false
|
|
12593
|
+
});
|
|
12495
12594
|
return finishLogin(ctx, pending);
|
|
12496
12595
|
};
|
|
12497
12596
|
var keyRows = (ctx, keys) => keys.map((key) => [
|
|
@@ -12602,7 +12701,19 @@ var revokeSignInById = async (ctx, id) => {
|
|
|
12602
12701
|
throw error;
|
|
12603
12702
|
}
|
|
12604
12703
|
};
|
|
12605
|
-
var
|
|
12704
|
+
var keyLabel = (key) => `"${key.name}" (${key.id})`;
|
|
12705
|
+
var wantsKeysKept = async (ctx, keys, options) => {
|
|
12706
|
+
if (options.keepKeys)
|
|
12707
|
+
return true;
|
|
12708
|
+
if (keys.length === 0 || ctx.opts.yes || !ctx.interactive)
|
|
12709
|
+
return false;
|
|
12710
|
+
ctx.note(`Logging out revokes the API keys this CLI created (${keys.length}):`);
|
|
12711
|
+
for (const key of keys)
|
|
12712
|
+
ctx.note(` ${key.org} ${keyLabel(key)} ${key.prefix}…`);
|
|
12713
|
+
ctx.note("Programs using them stop working. Answer N to keep them active.");
|
|
12714
|
+
return !await confirm(ctx.rt, `Revoke ${keys.length === 1 ? "this key" : `these ${keys.length} keys`} too?`);
|
|
12715
|
+
};
|
|
12716
|
+
var logout = async (ctx, options) => {
|
|
12606
12717
|
const config = await ctx.config();
|
|
12607
12718
|
const signIn = config.signIn;
|
|
12608
12719
|
const email = signIn?.email ?? null;
|
|
@@ -12610,8 +12721,10 @@ var logout = async (ctx) => {
|
|
|
12610
12721
|
const revokedKeys = [];
|
|
12611
12722
|
const failedKeys = [];
|
|
12612
12723
|
let signInRevoked = false;
|
|
12724
|
+
const candidates = config.keys.filter((entry) => !entry.kept);
|
|
12725
|
+
const keepKeys = usable ? await wantsKeysKept(ctx, candidates, options) : Boolean(options.keepKeys);
|
|
12613
12726
|
if (usable) {
|
|
12614
|
-
for (const entry of [
|
|
12727
|
+
for (const entry of keepKeys ? [] : candidates) {
|
|
12615
12728
|
const outcome = await revokeRecordedKey(ctx, entry);
|
|
12616
12729
|
if ("error" in outcome)
|
|
12617
12730
|
failedKeys.push(outcome);
|
|
@@ -12623,7 +12736,7 @@ var logout = async (ctx) => {
|
|
|
12623
12736
|
signInRevoked = true;
|
|
12624
12737
|
}
|
|
12625
12738
|
} else {
|
|
12626
|
-
for (const entry of
|
|
12739
|
+
for (const entry of keepKeys ? [] : candidates) {
|
|
12627
12740
|
failedKeys.push({
|
|
12628
12741
|
...factsOf(entry),
|
|
12629
12742
|
error: { code: "sign_in_needed", message: "Your sign-in is missing or expired." }
|
|
@@ -12635,27 +12748,40 @@ var logout = async (ctx) => {
|
|
|
12635
12748
|
current.defaultOrg = undefined;
|
|
12636
12749
|
current.organisations = undefined;
|
|
12637
12750
|
current.pending = [];
|
|
12751
|
+
if (keepKeys) {
|
|
12752
|
+
for (const entry of current.keys)
|
|
12753
|
+
entry.kept = true;
|
|
12754
|
+
}
|
|
12638
12755
|
});
|
|
12756
|
+
const signedOut = paint(ctx.out, "success", "Signed out");
|
|
12757
|
+
const keptKeys = (await ctx.config()).keys.filter((entry) => entry.kept).map((entry) => ({
|
|
12758
|
+
...factsOf(entry),
|
|
12759
|
+
written: { file: displayPath(ctx.rt, entry.file), name: entry.variable }
|
|
12760
|
+
}));
|
|
12639
12761
|
const lines = [];
|
|
12640
12762
|
if (!email)
|
|
12641
12763
|
lines.push("Not signed in.");
|
|
12642
12764
|
else if (signInRevoked)
|
|
12643
|
-
lines.push(
|
|
12765
|
+
lines.push(`${signedOut} ${email}. The sign-in is revoked.`);
|
|
12644
12766
|
else if (usable) {
|
|
12645
|
-
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.`);
|
|
12646
12768
|
} else {
|
|
12647
|
-
lines.push(
|
|
12769
|
+
lines.push(`${signedOut} ${email}. The sign-in was only removed from this computer; revoke it in the Deepdock dashboard.`);
|
|
12648
12770
|
}
|
|
12649
12771
|
if (revokedKeys.length > 0) {
|
|
12650
|
-
lines.push("Revoked the API keys this CLI created
|
|
12772
|
+
lines.push(`${paint(ctx.out, "success", "Revoked")} the API keys this CLI created:`);
|
|
12651
12773
|
for (const key of revokedKeys) {
|
|
12652
12774
|
const where = key.removed.removal === "done" ? ` ${describeRemoval(key.removed)}` : "";
|
|
12653
12775
|
lines.push(` ${key.org} "${key.name}" ${key.prefix}… ${key.id}${where}`);
|
|
12654
12776
|
}
|
|
12655
12777
|
lines.push("Programs still using one of them stop working now.");
|
|
12656
12778
|
}
|
|
12779
|
+
if (keptKeys.length > 0) {
|
|
12780
|
+
lines.push(`${paint(ctx.out, "success", "Kept")} ${keptKeys.length} API ${keptKeys.length === 1 ? "key" : "keys"} active: ${keptKeys.map(keyLabel).join(", ")}`);
|
|
12781
|
+
lines.push("Revoke one later with `deepdock keys revoke <id>` after `deepdock auth login`.");
|
|
12782
|
+
}
|
|
12657
12783
|
for (const key of failedKeys) {
|
|
12658
|
-
lines.push(
|
|
12784
|
+
lines.push(`${paint(ctx.out, "error", "Could not revoke")} "${key.name}" (${key.id}): run \`deepdock keys revoke ${key.id}\` after \`deepdock auth login\`.`);
|
|
12659
12785
|
}
|
|
12660
12786
|
const hints = [
|
|
12661
12787
|
...failedKeys.length > 0 ? ["Revoke the remaining keys with `deepdock keys revoke <id>`."] : [],
|
|
@@ -12666,6 +12792,7 @@ var logout = async (ctx) => {
|
|
|
12666
12792
|
revoked: signInRevoked,
|
|
12667
12793
|
email,
|
|
12668
12794
|
apiKeysRevoked: revokedKeys,
|
|
12795
|
+
apiKeysKept: keptKeys,
|
|
12669
12796
|
...failedKeys.length > 0 ? { apiKeysNotRevoked: failedKeys } : {}
|
|
12670
12797
|
},
|
|
12671
12798
|
meta: hints.length > 0 ? { hint: hints.join(" ") } : {},
|
|
@@ -12675,38 +12802,10 @@ var logout = async (ctx) => {
|
|
|
12675
12802
|
};
|
|
12676
12803
|
var registerAuth = (program, run) => {
|
|
12677
12804
|
const auth = program.command("auth").description("Sign in to Deepdock as yourself, see who is signed in, sign out");
|
|
12678
|
-
const loginCommand = auth.command("login").description("Sign in to Deepdock as yourself").option("--wait <request>", "finish a request started with --no-wait").option("--no-wait", "print the request and exit").action((options, cmd) => run(cmd, (ctx) => login(ctx, options)));
|
|
12805
|
+
const loginCommand = auth.command("login").description("Sign in to Deepdock as yourself").option("--wait <request>", "finish a request started with --no-wait").option("--no-wait", "print the request and exit").option("--no-browser", "do not open the link in a browser").action((options, cmd) => run(cmd, (ctx) => login(ctx, options)));
|
|
12679
12806
|
loginCommand.helpInformation = () => AUTH_LOGIN_HELP;
|
|
12680
12807
|
auth.command("status").description("Show who is signed in").action((_options, cmd) => run(cmd, status));
|
|
12681
|
-
auth.command("logout").description("Revoke the sign-in and the API keys this CLI created").action((
|
|
12682
|
-
};
|
|
12683
|
-
|
|
12684
|
-
// src/output/confirm.ts
|
|
12685
|
-
import { createInterface } from "node:readline";
|
|
12686
|
-
var ask = (rt, prompt) => new Promise((resolve) => {
|
|
12687
|
-
const rl = createInterface({ input: rt.stdin.stream, output: undefined, terminal: false });
|
|
12688
|
-
rt.stderr.write(prompt);
|
|
12689
|
-
let settled = false;
|
|
12690
|
-
rl.once("line", (line) => {
|
|
12691
|
-
settled = true;
|
|
12692
|
-
rl.close();
|
|
12693
|
-
resolve(line.trim());
|
|
12694
|
-
});
|
|
12695
|
-
rl.once("close", () => {
|
|
12696
|
-
if (!settled)
|
|
12697
|
-
resolve("");
|
|
12698
|
-
});
|
|
12699
|
-
});
|
|
12700
|
-
var confirm = async (rt, question) => {
|
|
12701
|
-
const answer = await ask(rt, `${question} [y/N]: `);
|
|
12702
|
-
return /^y(es)?$/i.test(answer);
|
|
12703
|
-
};
|
|
12704
|
-
var pickIndex = async (rt, count, defaultIndex) => {
|
|
12705
|
-
const answer = await ask(rt, `Number [${defaultIndex}]: `);
|
|
12706
|
-
const chosen = answer === "" ? defaultIndex : Number.parseInt(answer, 10);
|
|
12707
|
-
if (!Number.isInteger(chosen) || chosen < 1 || chosen > count)
|
|
12708
|
-
return;
|
|
12709
|
-
return chosen;
|
|
12808
|
+
auth.command("logout").description("Revoke the sign-in and the API keys this CLI created").option("--keep-keys", "revoke only the sign-in and leave the API keys active").action((options, cmd) => run(cmd, (ctx) => logout(ctx, options)));
|
|
12710
12809
|
};
|
|
12711
12810
|
|
|
12712
12811
|
// src/commands/devices.ts
|
|
@@ -12751,7 +12850,9 @@ var pair = async (ctx, code, label) => {
|
|
|
12751
12850
|
const device = isRecord(response.data) ? response.data : {};
|
|
12752
12851
|
const id = field(device, "id");
|
|
12753
12852
|
const name = field(device, "label");
|
|
12754
|
-
const lines = [
|
|
12853
|
+
const lines = [
|
|
12854
|
+
`${paint(ctx.out, "success", "Paired")} device ${id}${name ? ` (${stripControl(name)})` : ""}.`
|
|
12855
|
+
];
|
|
12755
12856
|
const status = field(device, "status");
|
|
12756
12857
|
if (status)
|
|
12757
12858
|
lines.push(` Status: ${status}`);
|
|
@@ -12773,7 +12874,7 @@ var rename = async (ctx, id, label) => {
|
|
|
12773
12874
|
return {
|
|
12774
12875
|
data: response.data,
|
|
12775
12876
|
meta: meta(response.requestId),
|
|
12776
|
-
text:
|
|
12877
|
+
text: `${paint(ctx.out, "success", "Renamed")} device ${deviceId} to ${stripControl(label)}.`
|
|
12777
12878
|
};
|
|
12778
12879
|
};
|
|
12779
12880
|
var release = async (ctx, id) => {
|
|
@@ -12802,7 +12903,7 @@ var release = async (ctx, id) => {
|
|
|
12802
12903
|
return {
|
|
12803
12904
|
data: response.data,
|
|
12804
12905
|
meta: meta(response.requestId),
|
|
12805
|
-
text:
|
|
12906
|
+
text: `${paint(ctx.out, "success", "Released")} device ${deviceId}.`
|
|
12806
12907
|
};
|
|
12807
12908
|
};
|
|
12808
12909
|
var renderSystems = (ctx, data) => {
|
|
@@ -12925,13 +13026,14 @@ var finishKey = async (ctx, pending, warned) => {
|
|
|
12925
13026
|
const file = displayPath(ctx.rt, pending.file);
|
|
12926
13027
|
const days = Math.round((Date.parse(created.expiresAt) - ctx.rt.now().getTime()) / DAY_MS);
|
|
12927
13028
|
const text = [
|
|
12928
|
-
|
|
13029
|
+
`${paint(ctx.out, "success", "Created")} API key "${stripControl(created.name)}".`,
|
|
12929
13030
|
` ID: ${created.id}`,
|
|
12930
13031
|
` Prefix: ${created.keyPrefix}`,
|
|
12931
13032
|
` Expires: ${created.expiresAt.slice(0, 10)} (${days} days)`,
|
|
12932
13033
|
` Written: ${file} (${pending.variable})`,
|
|
12933
13034
|
`The key value was shown to nobody and this CLI keeps no copy; your code reads it from ${file}.`,
|
|
12934
|
-
`
|
|
13035
|
+
`To revoke it: ${paint(ctx.out, "emphasis", `deepdock keys revoke ${created.id}`)}`,
|
|
13036
|
+
`${paint(ctx.out, "emphasis", "`deepdock auth logout`")} revokes this key; use ${paint(ctx.out, "emphasis", "`auth logout --keep-keys`")} to keep it.`
|
|
12935
13037
|
].join(`
|
|
12936
13038
|
`);
|
|
12937
13039
|
return {
|
|
@@ -12953,7 +13055,7 @@ var create = async (ctx, name, options) => {
|
|
|
12953
13055
|
if (!pending) {
|
|
12954
13056
|
throw usageError(`There is no pending key request ${options.wait}.`, "Start one with `deepdock keys create ... --no-wait`.");
|
|
12955
13057
|
}
|
|
12956
|
-
ctx
|
|
13058
|
+
noteWaiting(ctx);
|
|
12957
13059
|
return finishKey(ctx, pending, false);
|
|
12958
13060
|
}
|
|
12959
13061
|
if (name === undefined || name.trim() === "" || name.length > 100 || stripControl(name) !== name) {
|
|
@@ -12990,13 +13092,15 @@ var create = async (ctx, name, options) => {
|
|
|
12990
13092
|
await upsertPending(ctx, pending);
|
|
12991
13093
|
if (options.wait === false || !ctx.interactive) {
|
|
12992
13094
|
return pendingResult(pending, started.url, [
|
|
12993
|
-
`Open this link to approve the key: ${started.url}`,
|
|
13095
|
+
`Open this link to approve the key: ${ctx.out("link", started.url)}`,
|
|
12994
13096
|
`Then run \`deepdock keys create --wait ${started.request}\`.`
|
|
12995
13097
|
].join(`
|
|
12996
13098
|
`));
|
|
12997
13099
|
}
|
|
12998
|
-
announceApproval(ctx, "the key", started.url, started.expiresAt,
|
|
12999
|
-
|
|
13100
|
+
await announceApproval(ctx, "the key", started.url, started.expiresAt, {
|
|
13101
|
+
extra: `A human approves ${paint(ctx.err, "emphasis", `"Create an API key in ${org.name}?"`)} there; the page shows this computer's name.`,
|
|
13102
|
+
noBrowser: options.browser === false
|
|
13103
|
+
});
|
|
13000
13104
|
return finishKey(ctx, pending, true);
|
|
13001
13105
|
};
|
|
13002
13106
|
var revoke = async (ctx, id) => {
|
|
@@ -13038,7 +13142,7 @@ Every program using it stops working at once.`;
|
|
|
13038
13142
|
const removed = entry ? await settleWrittenLine(ctx, entry, true) : null;
|
|
13039
13143
|
const apiData = isRecord(response.data) ? response.data : {};
|
|
13040
13144
|
const label = entry ? `API key "${stripControl(entry.name)}"` : `API key ${keyId}`;
|
|
13041
|
-
const text = removed?.removal === "done" ?
|
|
13145
|
+
const text = removed?.removal === "done" ? `${paint(ctx.out, "success", "Revoked")} ${label} and ${describeRemoval(removed)}.` : `${paint(ctx.out, "success", "Revoked")} ${label}.`;
|
|
13042
13146
|
return {
|
|
13043
13147
|
data: { ...apiData, ...alreadyRevoked ? { alreadyRevoked: true } : {}, removed },
|
|
13044
13148
|
meta: { requestId: response.requestId },
|
|
@@ -13049,7 +13153,7 @@ var registerKeys = (program, run) => {
|
|
|
13049
13153
|
const keys = program.command("keys").description("Create, list and revoke your organisation's API keys");
|
|
13050
13154
|
keys.command("list").description("List the API keys of the organisation").action((_options, cmd) => run(cmd, list2));
|
|
13051
13155
|
const allowed = Object.keys(EXPIRY_DAYS).join("|");
|
|
13052
|
-
const createCommand = keys.command("create").description("Create an API key for your organisation (admin)").argument("[name]", "a name that says what the key is for").option("--expires <duration>", `${allowed}; 90 days is the maximum from the CLI`).option("--write-env <file>", "write DEEPDOCK_API_KEY=<key> into this env file").option("--wait <request>", "finish a request started with --no-wait").option("--no-wait", "print the request and exit").action((name, options, cmd) => run(cmd, (ctx) => create(ctx, name, options)));
|
|
13156
|
+
const createCommand = keys.command("create").description("Create an API key for your organisation (admin)").argument("[name]", "a name that says what the key is for").option("--expires <duration>", `${allowed}; 90 days is the maximum from the CLI`).option("--write-env <file>", "write DEEPDOCK_API_KEY=<key> into this env file").option("--wait <request>", "finish a request started with --no-wait").option("--no-wait", "print the request and exit").option("--no-browser", "do not open the link in a browser").action((name, options, cmd) => run(cmd, (ctx) => create(ctx, name, options)));
|
|
13053
13157
|
createCommand.helpInformation = () => KEYS_CREATE_HELP;
|
|
13054
13158
|
keys.command("revoke").description("Revoke an API key (admin)").argument("<id>", "the key id").action((id, _options, cmd) => run(cmd, (ctx) => revoke(ctx, id)));
|
|
13055
13159
|
};
|
|
@@ -13118,7 +13222,7 @@ var select = async (ctx, slug) => {
|
|
|
13118
13222
|
return {
|
|
13119
13223
|
data: { slug: found.slug, name: found.name, role: found.role },
|
|
13120
13224
|
meta: response.requestId ? { requestId: response.requestId } : {},
|
|
13121
|
-
text:
|
|
13225
|
+
text: `${paint(ctx.out, "success", "Default organisation:")} ${found.name} (${found.slug}).`
|
|
13122
13226
|
};
|
|
13123
13227
|
};
|
|
13124
13228
|
var registerOrg = (program, run) => {
|
package/package.json
CHANGED
|
@@ -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.
|
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
|
|
@@ -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
|