@datumos/datum-cli 0.0.0-stage → 0.588.0
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/LICENSE +201 -0
- package/NOTICE +8 -0
- package/README.md +36 -2
- package/cli/a2aCommand.js +225 -0
- package/cli/artifactsPublish.js +356 -0
- package/cli/authCommands.js +168 -0
- package/cli/bundleSignature.js +111 -0
- package/cli/commandTree.js +243 -0
- package/cli/credentialProcess.js +54 -0
- package/cli/credentials.js +93 -0
- package/cli/datum.js +814 -0
- package/cli/entraLogin.js +254 -0
- package/cli/help.js +509 -0
- package/cli/invoke.js +427 -0
- package/cli/mcpServe.js +361 -0
- package/cli/outputsDownload.js +126 -0
- package/cli/previewCommand.js +333 -0
- package/cli/satellitePairToken.js +48 -0
- package/cli/secureOrigin.js +37 -0
- package/cli/selfUpdate.js +779 -0
- package/cli/spaceManifestCommand.js +495 -0
- package/cli/spaceManifestDir.js +481 -0
- package/cli/spaceRepoTemplate.js +463 -0
- package/cli/updateCommand.js +184 -0
- package/npm-shrinkwrap.json +1231 -0
- package/openapi/spec.js +51199 -0
- package/package.json +27 -4
- package/spaces/reservedHostLabels.js +66 -0
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Derives the `datum` command tree from the OpenAPI spec — the CLI has NO
|
|
3
|
+
* hand-written command definitions, so it updates itself with every API
|
|
4
|
+
* change (the spec is the maintained contract, CLAUDE.md "API = kontrakt").
|
|
5
|
+
*
|
|
6
|
+
* Mapping rules (deterministic; commandTree.test.ts proves uniqueness
|
|
7
|
+
* against the real spec):
|
|
8
|
+
* - command tokens = static path segments after `/api`
|
|
9
|
+
* (`/api/cases/{id}/links` → `cases links`);
|
|
10
|
+
* - path params become positional arguments in path order;
|
|
11
|
+
* - the verb comes from the method + path shape:
|
|
12
|
+
* GET → `get` when the path ends with a param, else `list`;
|
|
13
|
+
* POST → the trailing action segment when there is one after the
|
|
14
|
+
* resource (`/api/cases/{id}/escalate` → `escalate`), else `create`;
|
|
15
|
+
* PATCH → `update`, PUT → `set`, DELETE → `delete`;
|
|
16
|
+
* - query params and top-level JSON body properties become `--kebab-case`
|
|
17
|
+
* flags (typed from their schema);
|
|
18
|
+
* - `x-cli-name` names a command the rules would give a taken name
|
|
19
|
+
* (X_CLI_NAME below); `x-cli: hidden` keeps an operation out;
|
|
20
|
+
* `x-cli-flag` on a body property names its flag (X_CLI_FLAG).
|
|
21
|
+
*/
|
|
22
|
+
/** OpenAPI extension on a BODY property that names its CLI flag instead of
|
|
23
|
+
* the kebab-cased wire name (#2905) — for a field whose derived flag would
|
|
24
|
+
* shadow a global one (RESERVED_GLOBAL_FLAGS): the page state's `data`
|
|
25
|
+
* becomes `--doc`. The wire name is unchanged. */
|
|
26
|
+
export const X_CLI_FLAG = "x-cli-flag";
|
|
27
|
+
const METHODS = ["get", "post", "put", "patch", "delete"];
|
|
28
|
+
/**
|
|
29
|
+
* Global CLI flag names — derived command flags must never collide with
|
|
30
|
+
* these (commandTree.test.ts checks the real spec; a colliding new API
|
|
31
|
+
* field should get a different wire name).
|
|
32
|
+
*/
|
|
33
|
+
export const RESERVED_GLOBAL_FLAGS = new Set([
|
|
34
|
+
"base-url",
|
|
35
|
+
"api-key",
|
|
36
|
+
"actor",
|
|
37
|
+
"data",
|
|
38
|
+
"fields",
|
|
39
|
+
"curl",
|
|
40
|
+
"raw",
|
|
41
|
+
"help",
|
|
42
|
+
"version",
|
|
43
|
+
]);
|
|
44
|
+
export function kebabCase(name) {
|
|
45
|
+
return name
|
|
46
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1-$2")
|
|
47
|
+
.replace(/[_\s]+/g, "-")
|
|
48
|
+
.toLowerCase();
|
|
49
|
+
}
|
|
50
|
+
function schemaType(schema) {
|
|
51
|
+
const t = schema?.type;
|
|
52
|
+
if (t === "number" ||
|
|
53
|
+
t === "integer" ||
|
|
54
|
+
t === "boolean" ||
|
|
55
|
+
t === "array" ||
|
|
56
|
+
t === "object") {
|
|
57
|
+
return t;
|
|
58
|
+
}
|
|
59
|
+
return "string";
|
|
60
|
+
}
|
|
61
|
+
function deriveVerb(method, segments) {
|
|
62
|
+
const tokens = segments.filter((s) => !s.startsWith("{"));
|
|
63
|
+
const endsWithParam = segments[segments.length - 1]?.startsWith("{") ?? false;
|
|
64
|
+
switch (method) {
|
|
65
|
+
case "get":
|
|
66
|
+
return { verb: endsWithParam ? "get" : "list", tokens };
|
|
67
|
+
case "post":
|
|
68
|
+
// A trailing static segment AFTER the resource is the action itself
|
|
69
|
+
// (`…/{id}/escalate` → `escalate`, `…/sync` → `sync`).
|
|
70
|
+
if (!endsWithParam && tokens.length > 1) {
|
|
71
|
+
return { verb: tokens[tokens.length - 1], tokens: tokens.slice(0, -1) };
|
|
72
|
+
}
|
|
73
|
+
return { verb: "create", tokens };
|
|
74
|
+
case "patch":
|
|
75
|
+
return { verb: "update", tokens };
|
|
76
|
+
case "put":
|
|
77
|
+
return { verb: "set", tokens };
|
|
78
|
+
case "delete":
|
|
79
|
+
return { verb: "delete", tokens };
|
|
80
|
+
default:
|
|
81
|
+
return { verb: method, tokens };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function buildCommand(method, path, op) {
|
|
85
|
+
// Segments after the leading "/api".
|
|
86
|
+
const segments = path.split("/").filter(Boolean).slice(1);
|
|
87
|
+
const derived = deriveVerb(method, segments);
|
|
88
|
+
const { verb, tokens } = cliNameOverride(method, path, op, derived.tokens) ?? derived;
|
|
89
|
+
const positionals = [];
|
|
90
|
+
const queryFlags = [];
|
|
91
|
+
for (const param of op.parameters ?? []) {
|
|
92
|
+
if (param.in === "path") {
|
|
93
|
+
positionals.push({
|
|
94
|
+
name: param.name,
|
|
95
|
+
description: param.description ?? "",
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
else if (param.in === "query") {
|
|
99
|
+
queryFlags.push({
|
|
100
|
+
flag: kebabCase(param.name),
|
|
101
|
+
name: param.name,
|
|
102
|
+
source: "query",
|
|
103
|
+
required: param.required ?? false,
|
|
104
|
+
type: schemaType(param.schema),
|
|
105
|
+
...(param.schema?.enum
|
|
106
|
+
? { enum: param.schema.enum.map(String) }
|
|
107
|
+
: {}),
|
|
108
|
+
description: param.description ?? "",
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const bodySchema = op.requestBody?.content?.["application/json"]?.schema;
|
|
113
|
+
const bodyFlags = [];
|
|
114
|
+
const requiredProps = new Set(bodySchema?.required ?? []);
|
|
115
|
+
for (const [name, schema] of Object.entries(bodySchema?.properties ?? {})) {
|
|
116
|
+
bodyFlags.push({
|
|
117
|
+
// `x-cli-flag` renames a body field whose derived flag would shadow a
|
|
118
|
+
// global one (the page state's `data` vs `--data`, #2905); the wire
|
|
119
|
+
// name stays.
|
|
120
|
+
flag: schema[X_CLI_FLAG] ?? kebabCase(name),
|
|
121
|
+
name,
|
|
122
|
+
source: "body",
|
|
123
|
+
required: requiredProps.has(name),
|
|
124
|
+
type: schemaType(schema),
|
|
125
|
+
...(schema.enum ? { enum: schema.enum.map(String) } : {}),
|
|
126
|
+
description: schema.description ?? "",
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
name: [...tokens, verb].join(" "),
|
|
131
|
+
tokens,
|
|
132
|
+
verb,
|
|
133
|
+
method: method.toUpperCase(),
|
|
134
|
+
path,
|
|
135
|
+
summary: op.summary ?? "",
|
|
136
|
+
description: op.description ?? "",
|
|
137
|
+
tags: op.tags ?? [],
|
|
138
|
+
positionals,
|
|
139
|
+
queryFlags,
|
|
140
|
+
bodyFlags,
|
|
141
|
+
hasBody: Boolean(op.requestBody),
|
|
142
|
+
bodyRequired: op.requestBody?.required ?? false,
|
|
143
|
+
bodyDescription: bodySchema?.description ?? "",
|
|
144
|
+
bodyOpen: bodySchema?.additionalProperties !== false && Boolean(bodySchema),
|
|
145
|
+
responses: Object.entries(op.responses ?? {}).map(([status, r]) => ({ status, description: r.description ?? "" })),
|
|
146
|
+
examples: cliExamples(method, path, op, [...tokens, verb].join(" ")),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
/** OpenAPI extension an operation sets to stay OUT of the CLI tree: a
|
|
150
|
+
* JSON-RPC endpoint (`/api/mcp`) that a flag-based command could not drive
|
|
151
|
+
* and whose name would shadow a hand-written built-in (`datum mcp serve`).
|
|
152
|
+
* The endpoint stays documented in the spec; only the CLI skips it. */
|
|
153
|
+
export const X_CLI_HIDDEN = "hidden";
|
|
154
|
+
/** OpenAPI extension that NAMES an operation's command instead of the
|
|
155
|
+
* derivation (#1562) — for the one case the rules cannot tell apart: a
|
|
156
|
+
* static segment where another path has a parameter
|
|
157
|
+
* (`GET /api/flows/runs` and `GET /api/flows/{id}/runs` both derive to
|
|
158
|
+
* `flows runs list`). The value is the whole command, space-separated —
|
|
159
|
+
* group tokens first, the verb last (`"flows runs all"`); it keeps the
|
|
160
|
+
* derived group (first token), so the command stays under `datum <group>`.
|
|
161
|
+
* Anything else is a spec bug and throws: the CLI must never guess. */
|
|
162
|
+
export const X_CLI_NAME = "x-cli-name";
|
|
163
|
+
const CLI_WORD = /^[a-z0-9][a-z0-9-]*$/;
|
|
164
|
+
function cliNameOverride(method, path, op, derivedTokens) {
|
|
165
|
+
const raw = op[X_CLI_NAME];
|
|
166
|
+
if (raw === undefined)
|
|
167
|
+
return null;
|
|
168
|
+
const where = `${method.toUpperCase()} ${path}: ${X_CLI_NAME}`;
|
|
169
|
+
if (typeof raw !== "string")
|
|
170
|
+
throw new Error(`${where} must be a string`);
|
|
171
|
+
const words = raw.trim().split(/\s+/).filter(Boolean);
|
|
172
|
+
if (words.length < 2) {
|
|
173
|
+
throw new Error(`${where} "${raw}" needs a group and a verb ("flows runs all")`);
|
|
174
|
+
}
|
|
175
|
+
const bad = words.find((w) => !CLI_WORD.test(w));
|
|
176
|
+
if (bad !== undefined) {
|
|
177
|
+
throw new Error(`${where} "${raw}": "${bad}" is not a lowercase command word`);
|
|
178
|
+
}
|
|
179
|
+
if (derivedTokens[0] !== undefined && words[0] !== derivedTokens[0]) {
|
|
180
|
+
throw new Error(`${where} "${raw}" must stay in the "${derivedTokens[0]}" group`);
|
|
181
|
+
}
|
|
182
|
+
return { verb: words[words.length - 1], tokens: words.slice(0, -1) };
|
|
183
|
+
}
|
|
184
|
+
/** OpenAPI extension listing hand-picked invocations of an operation's
|
|
185
|
+
* command (#1562) — what `--help` and cli-reference.md show under
|
|
186
|
+
* *Examples* when a command's useful calls are not its required flags
|
|
187
|
+
* (`datum flows runs all --attention`). Each entry is a whole command line
|
|
188
|
+
* that starts with `datum <the command's name>`; anything else is a spec
|
|
189
|
+
* bug and throws, like a malformed `x-cli-name`. */
|
|
190
|
+
export const X_CLI_EXAMPLES = "x-cli-examples";
|
|
191
|
+
function cliExamples(method, path, op, name) {
|
|
192
|
+
const raw = op[X_CLI_EXAMPLES];
|
|
193
|
+
if (raw === undefined)
|
|
194
|
+
return [];
|
|
195
|
+
const where = `${method.toUpperCase()} ${path}: ${X_CLI_EXAMPLES}`;
|
|
196
|
+
if (!Array.isArray(raw) || raw.length === 0) {
|
|
197
|
+
throw new Error(`${where} must be a non-empty list of command lines`);
|
|
198
|
+
}
|
|
199
|
+
return raw.map((example) => {
|
|
200
|
+
if (typeof example !== "string")
|
|
201
|
+
throw new Error(`${where} entries must be strings`);
|
|
202
|
+
const line = example.trim().replace(/\s+/g, " ");
|
|
203
|
+
if (line !== `datum ${name}` && !line.startsWith(`datum ${name} `)) {
|
|
204
|
+
throw new Error(`${where} "${example}" must start with "datum ${name}"`);
|
|
205
|
+
}
|
|
206
|
+
return line;
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
/** All CLI commands derived from the spec, sorted by name. */
|
|
210
|
+
export function buildCommandTree(spec) {
|
|
211
|
+
const commands = [];
|
|
212
|
+
for (const [path, operations] of Object.entries(spec.paths)) {
|
|
213
|
+
for (const method of METHODS) {
|
|
214
|
+
const op = operations[method];
|
|
215
|
+
if (op && op["x-cli"] !== X_CLI_HIDDEN) {
|
|
216
|
+
commands.push(buildCommand(method, path, op));
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return commands.sort((a, b) => a.name.localeCompare(b.name));
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Matches argv words against the tree: the longest command whose
|
|
224
|
+
* `name` tokens are a prefix of the given words wins. Returns the command
|
|
225
|
+
* and the remaining words (positionals/flags).
|
|
226
|
+
*/
|
|
227
|
+
export function matchCommand(commands, words) {
|
|
228
|
+
let best = null;
|
|
229
|
+
for (const command of commands) {
|
|
230
|
+
const parts = command.name.split(" ");
|
|
231
|
+
if (parts.length > words.length)
|
|
232
|
+
continue;
|
|
233
|
+
if (parts.every((p, i) => p === words[i])) {
|
|
234
|
+
if (!best || parts.length > best.length) {
|
|
235
|
+
best = { command, length: parts.length };
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return best
|
|
240
|
+
? { command: best.command, rest: words.slice(best.length) }
|
|
241
|
+
: null;
|
|
242
|
+
}
|
|
243
|
+
//# sourceMappingURL=commandTree.js.map
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
/**
|
|
3
|
+
* The CLI's credential process (#2719) — the `credential_process` pattern
|
|
4
|
+
* of the AWS CLI (#111): `DATUM_CREDENTIAL_PROCESS` names a command whose
|
|
5
|
+
* stdout is `{ "token": "datum_…", "expiresAt": "<ISO>" }`. The CLI runs it
|
|
6
|
+
* once per invocation and uses the token as its API key; nothing is
|
|
7
|
+
* written to the CLI's own files. On a personal agent's machine the
|
|
8
|
+
* runtime points it at the run plane's /datum-token (the person's own
|
|
9
|
+
* 15-minute token), so no long-lived secret lies on the disk.
|
|
10
|
+
*
|
|
11
|
+
* Precedence: `--api-key` > DATUM_CREDENTIAL_PROCESS > DATUM_API_KEY >
|
|
12
|
+
* the stored sign-in (docs/cli.md "Auth").
|
|
13
|
+
*/
|
|
14
|
+
export const CREDENTIAL_PROCESS_ENV = "DATUM_CREDENTIAL_PROCESS";
|
|
15
|
+
/** How long the command may take (it makes one HTTPS call). */
|
|
16
|
+
export const CREDENTIAL_PROCESS_TIMEOUT_MS = 30_000;
|
|
17
|
+
export class CredentialProcessError extends Error {
|
|
18
|
+
}
|
|
19
|
+
const defaultRunner = (command) => {
|
|
20
|
+
// The value is the run's own configuration (the runtime writes it, or the
|
|
21
|
+
// person sets it); it runs under a shell like the AWS CLI's.
|
|
22
|
+
const out = spawnSync(command, {
|
|
23
|
+
shell: true,
|
|
24
|
+
encoding: "utf8",
|
|
25
|
+
timeout: CREDENTIAL_PROCESS_TIMEOUT_MS,
|
|
26
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
27
|
+
});
|
|
28
|
+
return { status: out.status, stdout: out.stdout ?? "", stderr: out.stderr ?? "", ...(out.error ? { error: out.error } : {}) };
|
|
29
|
+
};
|
|
30
|
+
/** Runs the credential process and reads its answer. Throws
|
|
31
|
+
* CredentialProcessError with a message for the person — never the
|
|
32
|
+
* process's stdout (it may carry the token). */
|
|
33
|
+
export function readCredentialProcess(command, run = defaultRunner) {
|
|
34
|
+
const res = run(command);
|
|
35
|
+
if (res.error)
|
|
36
|
+
throw new CredentialProcessError(`${CREDENTIAL_PROCESS_ENV} could not run: ${res.error.message}`);
|
|
37
|
+
if (res.status !== 0) {
|
|
38
|
+
const why = res.stderr.trim().split("\n").slice(-1)[0] ?? "";
|
|
39
|
+
throw new CredentialProcessError(`${CREDENTIAL_PROCESS_ENV} exited ${res.status ?? "?"}${why ? ` — ${why.slice(0, 300)}` : ""}`);
|
|
40
|
+
}
|
|
41
|
+
let parsed;
|
|
42
|
+
try {
|
|
43
|
+
parsed = JSON.parse(res.stdout);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
throw new CredentialProcessError(`${CREDENTIAL_PROCESS_ENV} did not print JSON { token, expiresAt }`);
|
|
47
|
+
}
|
|
48
|
+
const p = (parsed && typeof parsed === "object" ? parsed : {});
|
|
49
|
+
const token = typeof p.token === "string" ? p.token.trim() : "";
|
|
50
|
+
if (!token)
|
|
51
|
+
throw new CredentialProcessError(`${CREDENTIAL_PROCESS_ENV} printed no token`);
|
|
52
|
+
return { token, expiresAt: typeof p.expiresAt === "string" ? p.expiresAt : "" };
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=credentialProcess.js.map
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
/**
|
|
5
|
+
* Where `datum login` keeps what it obtained (docs/developer-access.md):
|
|
6
|
+
* ONE JSON file, mode 0600 in a 0700 directory — the same posture `gh`
|
|
7
|
+
* takes on Linux. No keychain: the CLI is dependency-free and headless
|
|
8
|
+
* (ssh, containers) by design; the refresh token is the sensitive part and
|
|
9
|
+
* it lives only here, never in env or in a shell history.
|
|
10
|
+
*
|
|
11
|
+
* Location: `$DATUM_CONFIG_DIR/credentials.json`, else
|
|
12
|
+
* `$XDG_CONFIG_HOME/datum/credentials.json`, else `~/.config/datum/…`.
|
|
13
|
+
* Keyed by the ORG origin the login ran against (`https://datum.example`);
|
|
14
|
+
* space subdomains reuse the org's tokens — the token is the same
|
|
15
|
+
* person on the same app registration whichever space host answers.
|
|
16
|
+
*/
|
|
17
|
+
export const CREDENTIALS_FILE = "credentials.json";
|
|
18
|
+
export const CREDENTIALS_VERSION = 1;
|
|
19
|
+
const EMPTY = { version: CREDENTIALS_VERSION, logins: {} };
|
|
20
|
+
export function credentialsDir(env = process.env) {
|
|
21
|
+
if (env.DATUM_CONFIG_DIR?.trim())
|
|
22
|
+
return env.DATUM_CONFIG_DIR.trim();
|
|
23
|
+
const xdg = env.XDG_CONFIG_HOME?.trim();
|
|
24
|
+
return path.join(xdg || path.join(homedir(), ".config"), "datum");
|
|
25
|
+
}
|
|
26
|
+
export function credentialsPath(env = process.env) {
|
|
27
|
+
return path.join(credentialsDir(env), CREDENTIALS_FILE);
|
|
28
|
+
}
|
|
29
|
+
/** The credentials key for a base URL: scheme + host + port, no path. */
|
|
30
|
+
export function originOf(baseUrl) {
|
|
31
|
+
return new URL(baseUrl).origin;
|
|
32
|
+
}
|
|
33
|
+
export class CredentialStore {
|
|
34
|
+
file;
|
|
35
|
+
constructor(file = credentialsPath()) {
|
|
36
|
+
this.file = file;
|
|
37
|
+
}
|
|
38
|
+
get path() {
|
|
39
|
+
return this.file;
|
|
40
|
+
}
|
|
41
|
+
read() {
|
|
42
|
+
if (!existsSync(this.file))
|
|
43
|
+
return { ...EMPTY, logins: {} };
|
|
44
|
+
try {
|
|
45
|
+
const parsed = JSON.parse(readFileSync(this.file, "utf8"));
|
|
46
|
+
if (parsed.version !== CREDENTIALS_VERSION || typeof parsed.logins !== "object" || !parsed.logins) {
|
|
47
|
+
return { ...EMPTY, logins: {} };
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
version: CREDENTIALS_VERSION,
|
|
51
|
+
...(parsed.defaultBaseUrl ? { defaultBaseUrl: parsed.defaultBaseUrl } : {}),
|
|
52
|
+
logins: parsed.logins,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
// A corrupt file must not brick the CLI — the next login rewrites it.
|
|
57
|
+
return { ...EMPTY, logins: {} };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
write(file) {
|
|
61
|
+
const dir = path.dirname(this.file);
|
|
62
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
63
|
+
const tmp = `${this.file}.${process.pid}.tmp`;
|
|
64
|
+
writeFileSync(tmp, `${JSON.stringify(file, null, 2)}\n`, { mode: 0o600 });
|
|
65
|
+
chmodSync(tmp, 0o600);
|
|
66
|
+
renameSync(tmp, this.file);
|
|
67
|
+
}
|
|
68
|
+
get(baseUrl) {
|
|
69
|
+
return this.read().logins[originOf(baseUrl)] ?? null;
|
|
70
|
+
}
|
|
71
|
+
set(login) {
|
|
72
|
+
const file = this.read();
|
|
73
|
+
const origin = originOf(login.baseUrl);
|
|
74
|
+
file.logins[origin] = { ...login, baseUrl: origin };
|
|
75
|
+
file.defaultBaseUrl = origin;
|
|
76
|
+
this.write(file);
|
|
77
|
+
}
|
|
78
|
+
/** Forgets one login; returns whether there was one. */
|
|
79
|
+
delete(baseUrl) {
|
|
80
|
+
const file = this.read();
|
|
81
|
+
const origin = originOf(baseUrl);
|
|
82
|
+
const had = origin in file.logins;
|
|
83
|
+
delete file.logins[origin];
|
|
84
|
+
if (file.defaultBaseUrl === origin)
|
|
85
|
+
delete file.defaultBaseUrl;
|
|
86
|
+
this.write(file);
|
|
87
|
+
return had;
|
|
88
|
+
}
|
|
89
|
+
defaultBaseUrl() {
|
|
90
|
+
return this.read().defaultBaseUrl ?? null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=credentials.js.map
|