dropsh 0.6.1 → 0.7.1
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/README.md +92 -1
- package/dist/src/commands/connections.d.ts +37 -0
- package/dist/src/commands/connections.js +42 -0
- package/dist/src/core/auth/session-store.d.ts +2 -1
- package/dist/src/core/auth/session-store.js +4 -4
- package/dist/src/core/config.d.ts +2 -0
- package/dist/src/core/config.js +30 -3
- package/dist/src/core/connections.d.ts +58 -0
- package/dist/src/core/connections.js +115 -0
- package/dist/src/core/paths.d.ts +8 -0
- package/dist/src/core/paths.js +14 -0
- package/dist/src/index.d.ts +12 -0
- package/dist/src/index.js +84 -13
- package/dist/src/plugin-api.d.ts +2 -0
- package/dist/src/plugin-api.js +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -171,7 +171,8 @@ Each descriptor's `with` is passed to the plugin factory (legacy alias:
|
|
|
171
171
|
descriptor names `dropsh/plugin` with `export: "basicAuthPlugin"`.
|
|
172
172
|
|
|
173
173
|
Use `dropsh.config.example.js` as a fuller starting point. Override the config
|
|
174
|
-
path with `--config <path>` or `DROPSH_CONFIG
|
|
174
|
+
path with `--config <path>` or `DROPSH_CONFIG`, or address a config file by name
|
|
175
|
+
with `--connection <id>` — see [Named connections](#named-connections--one-config-file-per-site).
|
|
175
176
|
|
|
176
177
|
<details>
|
|
177
178
|
<summary>Workspace variant — importing factories directly</summary>
|
|
@@ -204,6 +205,66 @@ to the config file, so it only works when the packages are installed there.
|
|
|
204
205
|
|
|
205
206
|
</details>
|
|
206
207
|
|
|
208
|
+
### Named connections — one config file per site
|
|
209
|
+
|
|
210
|
+
A *named connection* is a config file addressed by id instead of by path. A
|
|
211
|
+
connection is a file: `<connections-dir>/<id>.js`, carrying exactly the shape
|
|
212
|
+
`dropsh.config.js` has (`site`, `defaults`, `plugins`), and its basename is its
|
|
213
|
+
id. There is no registry, index or map — adding a connection is adding a file.
|
|
214
|
+
Only `.js` files are connections.
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
dropsh --connection staging read node/article/<uuid>
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
**The connections directory** defaults to `~/.config/dropsh/connections/`,
|
|
221
|
+
beside the per-host auth store. It is overridable, and the flag wins over the
|
|
222
|
+
environment variable:
|
|
223
|
+
|
|
224
|
+
| # | Selector | Kind |
|
|
225
|
+
|---|----------|------|
|
|
226
|
+
| 1 | `--connections-dir <path>` | flag |
|
|
227
|
+
| 2 | `$DROPSH_CONNECTIONS_DIR` | environment |
|
|
228
|
+
| 3 | `~/.config/dropsh/connections` | default |
|
|
229
|
+
|
|
230
|
+
**Which config file a run uses**, highest first:
|
|
231
|
+
|
|
232
|
+
| # | Selector | Kind |
|
|
233
|
+
|---|----------|------|
|
|
234
|
+
| 1 | `--config <path>` | names a **file** |
|
|
235
|
+
| 2 | `--connection <id>` | names an **entry** |
|
|
236
|
+
| 3 | `$DROPSH_CONFIG` | names a **file** |
|
|
237
|
+
| 4 | `$DROPSH_CONNECTION` | names an **entry** |
|
|
238
|
+
| 5 | `./dropsh.config.js` | the cwd default |
|
|
239
|
+
|
|
240
|
+
`--config` and `--connection` given together is not an error: `--config` names a
|
|
241
|
+
file and wins.
|
|
242
|
+
|
|
243
|
+
An unknown id is a hard error — non-zero exit, naming the id, the directory
|
|
244
|
+
searched and the available ids. Nothing is prompted and nothing is created, so
|
|
245
|
+
an unattended run fails rather than hangs:
|
|
246
|
+
|
|
247
|
+
```
|
|
248
|
+
Unknown connection 'stagng'. Available in /home/me/.config/dropsh/connections: production, staging.
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
**List them** with `dropsh connections list`: one row per connection with its
|
|
252
|
+
`site.base_url`, a `*` on the one the current invocation would resolve to, and
|
|
253
|
+
the resolved path stated instead when that is a path rather than an id. A
|
|
254
|
+
connection whose file cannot be read is listed with its error in place of the
|
|
255
|
+
URL and the listing still exits 0. Listing reads `site` only, so a connection
|
|
256
|
+
naming an uninstalled plugin package cannot abort the listing of the others.
|
|
257
|
+
|
|
258
|
+
```bash
|
|
259
|
+
dropsh connections list
|
|
260
|
+
dropsh connections list --format json
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
Auth is unchanged by connections: the session store keys on the host of
|
|
264
|
+
`site.base_url`, so two connections against different hosts keep their tokens
|
|
265
|
+
apart and two against the same host share them. `--auth-profile` keeps selecting
|
|
266
|
+
the identity *within* whichever connection is in force.
|
|
267
|
+
|
|
207
268
|
## Authentication
|
|
208
269
|
|
|
209
270
|
Authentication is configured through plugins. Each plugin that contributes an
|
|
@@ -301,8 +362,13 @@ dropsh delete <entity_type>/<bundle>/<uuid> [--dry-run]
|
|
|
301
362
|
dropsh upload-file --target=<entity_type>/<bundle>/<uuid>/<field> --file=<path> [--dry-run]
|
|
302
363
|
dropsh schema [--refresh]
|
|
303
364
|
dropsh schema <entity_type>/<bundle> [--for=create|update] [--refresh] [--fields]
|
|
365
|
+
dropsh connections list
|
|
304
366
|
```
|
|
305
367
|
|
|
368
|
+
Every command accepts the global `--config <path>`, `--connection <id>` and
|
|
369
|
+
`--connections-dir <path>` selectors described under
|
|
370
|
+
[Named connections](#named-connections--one-config-file-per-site).
|
|
371
|
+
|
|
306
372
|
`create` and `update` validate payloads against the current schema by default.
|
|
307
373
|
Pass `--no-validate` when you intentionally want to skip local validation.
|
|
308
374
|
|
|
@@ -596,6 +662,31 @@ export default {
|
|
|
596
662
|
`@dropsh/plugin-*` packages), so `import { DropSHPlugin } from "dropsh/plugin"`
|
|
597
663
|
is typed with no ambient `declare module`.
|
|
598
664
|
|
|
665
|
+
The connection-resolution API is available to embedders as well:
|
|
666
|
+
|
|
667
|
+
```ts
|
|
668
|
+
import {
|
|
669
|
+
resolveConfigSource,
|
|
670
|
+
resolveConnectionsDir,
|
|
671
|
+
} from "dropsh/plugin";
|
|
672
|
+
|
|
673
|
+
const connectionsDir = resolveConnectionsDir({
|
|
674
|
+
flag: "/home/me/.config/dropsh/connections",
|
|
675
|
+
});
|
|
676
|
+
const config = resolveConfigSource({
|
|
677
|
+
connection: "staging",
|
|
678
|
+
connectionsDir,
|
|
679
|
+
});
|
|
680
|
+
// config.path and config.source identify the selected config file and selector.
|
|
681
|
+
```
|
|
682
|
+
|
|
683
|
+
The same entry point also exports `CWD_CONFIG_FILE`, `listConnectionIds`, and
|
|
684
|
+
`listConnections`, plus the `ConfigSource`, `ConfigSourceKind`,
|
|
685
|
+
`ConnectionEntry`, `ResolveConfigSourceInput`, and `ResolveConnectionsDirInput`
|
|
686
|
+
types. These exports use the same selector precedence and connections-directory
|
|
687
|
+
semantics as the CLI; see [Named connections](#named-connections--one-config-file-per-site)
|
|
688
|
+
for the full documentation.
|
|
689
|
+
|
|
599
690
|
### Composing plugins
|
|
600
691
|
|
|
601
692
|
`composePlugins(...children)` bundles several plugins into one flat list, so an
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { ConfigSource, ConnectionEntry } from "../core/connections.js";
|
|
2
|
+
export interface ConnectionsListArgs {
|
|
3
|
+
/** The resolved connections directory. */
|
|
4
|
+
dir: string;
|
|
5
|
+
/** What this invocation would resolve to — the row it marks, when it names an id. */
|
|
6
|
+
resolved: ConfigSource;
|
|
7
|
+
}
|
|
8
|
+
export interface ConnectionsListDeps {
|
|
9
|
+
list: (dir: string) => Promise<ConnectionEntry[]>;
|
|
10
|
+
emit: (v: unknown) => void | Promise<void>;
|
|
11
|
+
}
|
|
12
|
+
/** One listed connection, as it is emitted. */
|
|
13
|
+
export interface ConnectionsListRow {
|
|
14
|
+
id: string;
|
|
15
|
+
base_url?: string;
|
|
16
|
+
error?: string;
|
|
17
|
+
/** True on the single connection this invocation would resolve to. */
|
|
18
|
+
current: boolean;
|
|
19
|
+
}
|
|
20
|
+
export interface ConnectionsListResult {
|
|
21
|
+
directory: string;
|
|
22
|
+
resolved: ConfigSource;
|
|
23
|
+
connections: ConnectionsListRow[];
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* List the connections in the resolved directory. A broken connection is a row
|
|
27
|
+
* carrying its error, not an abort: the listing succeeded and reports exactly
|
|
28
|
+
* what is on disk. When the invocation resolves to a path rather than an id —
|
|
29
|
+
* `--config`, `$DROPSH_CONFIG`, or the cwd default — no row is marked and the
|
|
30
|
+
* resolved path stands on its own in `resolved`.
|
|
31
|
+
*/
|
|
32
|
+
export declare function runConnectionsList(args: ConnectionsListArgs, deps: ConnectionsListDeps): Promise<ConnectionsListResult>;
|
|
33
|
+
/**
|
|
34
|
+
* The listing as a human reads it: one row per connection, a `*` on the one in
|
|
35
|
+
* force, and the resolved path stated below when no id is in force.
|
|
36
|
+
*/
|
|
37
|
+
export declare function renderConnectionsTable(result: ConnectionsListResult): string;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* List the connections in the resolved directory. A broken connection is a row
|
|
3
|
+
* carrying its error, not an abort: the listing succeeded and reports exactly
|
|
4
|
+
* what is on disk. When the invocation resolves to a path rather than an id —
|
|
5
|
+
* `--config`, `$DROPSH_CONFIG`, or the cwd default — no row is marked and the
|
|
6
|
+
* resolved path stands on its own in `resolved`.
|
|
7
|
+
*/
|
|
8
|
+
export async function runConnectionsList(args, deps) {
|
|
9
|
+
const entries = await deps.list(args.dir);
|
|
10
|
+
const result = {
|
|
11
|
+
directory: args.dir,
|
|
12
|
+
resolved: args.resolved,
|
|
13
|
+
connections: entries.map((e) => ({
|
|
14
|
+
id: e.id,
|
|
15
|
+
...(e.baseUrl !== undefined ? { base_url: e.baseUrl } : {}),
|
|
16
|
+
...(e.error !== undefined ? { error: e.error } : {}),
|
|
17
|
+
current: e.id === args.resolved.id,
|
|
18
|
+
})),
|
|
19
|
+
};
|
|
20
|
+
await deps.emit(result);
|
|
21
|
+
return result;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The listing as a human reads it: one row per connection, a `*` on the one in
|
|
25
|
+
* force, and the resolved path stated below when no id is in force.
|
|
26
|
+
*/
|
|
27
|
+
export function renderConnectionsTable(result) {
|
|
28
|
+
const lines = [`Connections in ${result.directory}`];
|
|
29
|
+
if (result.connections.length === 0) {
|
|
30
|
+
lines.push(" (none)");
|
|
31
|
+
}
|
|
32
|
+
const width = Math.max(0, ...result.connections.map((c) => c.id.length));
|
|
33
|
+
for (const c of result.connections) {
|
|
34
|
+
const marker = c.current ? "*" : " ";
|
|
35
|
+
const value = c.base_url ?? `! ${c.error ?? "unreadable"}`;
|
|
36
|
+
lines.push(`${marker} ${c.id.padEnd(width)} ${value}`);
|
|
37
|
+
}
|
|
38
|
+
if (result.resolved.id === undefined) {
|
|
39
|
+
lines.push(`Resolved config: ${result.resolved.path} (${result.resolved.source})`);
|
|
40
|
+
}
|
|
41
|
+
return lines.join("\n");
|
|
42
|
+
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { defaultStateDir } from "../paths.js";
|
|
1
2
|
import type { AuthSession } from "./types.js";
|
|
3
|
+
export { defaultStateDir };
|
|
2
4
|
/** One stored auth identity: the provider that owns it plus its opaque session. */
|
|
3
5
|
export interface ProfileRecord {
|
|
4
6
|
provider: string;
|
|
@@ -17,7 +19,6 @@ export interface SessionRecord {
|
|
|
17
19
|
activeProvider: string;
|
|
18
20
|
session: AuthSession;
|
|
19
21
|
}
|
|
20
|
-
export declare function defaultStateDir(): string;
|
|
21
22
|
/**
|
|
22
23
|
* Read the whole profiles file, migrating a legacy `{ activeProvider, session }`
|
|
23
24
|
* record in memory to the v2 shape. No write-time migration is needed: the next
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { chmod, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
2
|
-
import { homedir } from "node:os";
|
|
3
2
|
import { join } from "node:path";
|
|
4
3
|
import { ConfigError } from "../../errors.js";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
import { defaultStateDir } from "../paths.js";
|
|
5
|
+
// Re-exported so every existing importer of the auth store keeps working while
|
|
6
|
+
// the path itself lives in one place (`src/core/paths.ts`).
|
|
7
|
+
export { defaultStateDir };
|
|
8
8
|
function hostnameFromUrl(baseUrl) {
|
|
9
9
|
return new URL(baseUrl).hostname;
|
|
10
10
|
}
|
|
@@ -11,4 +11,6 @@ export interface Config {
|
|
|
11
11
|
};
|
|
12
12
|
plugins: DropSHPlugin[];
|
|
13
13
|
}
|
|
14
|
+
/** The `site` section of a config file, read without resolving its plugins. */
|
|
15
|
+
export declare function loadConfigSite(filePath: string): Promise<SiteConfig>;
|
|
14
16
|
export declare function loadConfig(filePath: string): Promise<Config>;
|
package/dist/src/core/config.js
CHANGED
|
@@ -152,7 +152,14 @@ async function resolvePlugins(raw, configPath) {
|
|
|
152
152
|
await expand(entry, baseBases, undefined);
|
|
153
153
|
return out;
|
|
154
154
|
}
|
|
155
|
-
|
|
155
|
+
/**
|
|
156
|
+
* Import a config module and validate its `site` section — everything a config
|
|
157
|
+
* source has to be valid for, short of resolving `plugins[]`. Both `loadConfig`
|
|
158
|
+
* and the connection listing go through here, so the two cannot diverge on what
|
|
159
|
+
* a valid `site` is, and the listing can read many files without constructing
|
|
160
|
+
* any plugin.
|
|
161
|
+
*/
|
|
162
|
+
async function importConfig(filePath) {
|
|
156
163
|
const configPath = resolve(filePath);
|
|
157
164
|
let mod;
|
|
158
165
|
try {
|
|
@@ -169,13 +176,33 @@ export async function loadConfig(filePath) {
|
|
|
169
176
|
throw new ConfigError("site section missing");
|
|
170
177
|
if (typeof site.base_url !== "string" || site.base_url.length === 0)
|
|
171
178
|
throw new ConfigError("site.base_url required");
|
|
172
|
-
|
|
173
|
-
|
|
179
|
+
// Reject rather than silently trim, so the persisted config stays canonical.
|
|
180
|
+
// A trailing slash survives into every string-concatenated URL a consumer
|
|
181
|
+
// builds from base_url — notably `${base_url}/oauth/token` — where the doubled
|
|
182
|
+
// slash addresses a route that is not the token endpoint (DROPSH-13).
|
|
183
|
+
if (site.base_url.endsWith("/")) {
|
|
184
|
+
const corrected = site.base_url.replace(/\/+$/, "");
|
|
185
|
+
throw new ConfigError(`site.base_url must not end with a trailing slash — set it to ${corrected}`);
|
|
186
|
+
}
|
|
174
187
|
return {
|
|
188
|
+
configPath,
|
|
189
|
+
raw,
|
|
175
190
|
site: {
|
|
176
191
|
base_url: site.base_url,
|
|
177
192
|
jsonapi_prefix: typeof site.jsonapi_prefix === "string" ? site.jsonapi_prefix : "/jsonapi",
|
|
178
193
|
},
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
/** The `site` section of a config file, read without resolving its plugins. */
|
|
197
|
+
export async function loadConfigSite(filePath) {
|
|
198
|
+
return (await importConfig(filePath)).site;
|
|
199
|
+
}
|
|
200
|
+
export async function loadConfig(filePath) {
|
|
201
|
+
const { configPath, raw, site } = await importConfig(filePath);
|
|
202
|
+
const defaults = isRecord(raw.defaults) ? raw.defaults : {};
|
|
203
|
+
const plugins = await resolvePlugins(raw.plugins, configPath);
|
|
204
|
+
return {
|
|
205
|
+
site,
|
|
179
206
|
defaults: {
|
|
180
207
|
dry_run: defaults.dry_run === true,
|
|
181
208
|
timeout_ms: typeof defaults.timeout_ms === "number" ? defaults.timeout_ms : 30000,
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/** The name of the config file dropsh falls back to in the current directory. */
|
|
2
|
+
export declare const CWD_CONFIG_FILE = "dropsh.config.js";
|
|
3
|
+
/** Which selector produced the resolved config path. */
|
|
4
|
+
export type ConfigSourceKind = "flag-config" | "flag-connection" | "env-config" | "env-connection" | "cwd";
|
|
5
|
+
/** The chosen config file plus the provenance of that choice. */
|
|
6
|
+
export interface ConfigSource {
|
|
7
|
+
path: string;
|
|
8
|
+
source: ConfigSourceKind;
|
|
9
|
+
/** The connection id, when the winning selector named an entry rather than a file. */
|
|
10
|
+
id?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface ResolveConnectionsDirInput {
|
|
13
|
+
/** `--connections-dir <path>`. */
|
|
14
|
+
flag?: string | undefined;
|
|
15
|
+
env?: NodeJS.ProcessEnv;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The connections directory: `--connections-dir` > `$DROPSH_CONNECTIONS_DIR` >
|
|
19
|
+
* `~/.config/dropsh/connections`. Resolved independently of which config
|
|
20
|
+
* selector wins, because `connections list` reports the directory either way.
|
|
21
|
+
*/
|
|
22
|
+
export declare function resolveConnectionsDir(input?: ResolveConnectionsDirInput): string;
|
|
23
|
+
export interface ResolveConfigSourceInput {
|
|
24
|
+
/** `--config <path>`. */
|
|
25
|
+
config?: string | undefined;
|
|
26
|
+
/** `--connection <id>`. */
|
|
27
|
+
connection?: string | undefined;
|
|
28
|
+
connectionsDir?: string;
|
|
29
|
+
env?: NodeJS.ProcessEnv;
|
|
30
|
+
cwd?: string;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Resolve the config file for this invocation, highest selector first:
|
|
34
|
+
* `--config` > `--connection` > `$DROPSH_CONFIG` > `$DROPSH_CONNECTION` >
|
|
35
|
+
* `./dropsh.config.js`. `--config` and `--connection` together is not an error:
|
|
36
|
+
* `--config` names a *file* and that is its stated meaning, so it wins silently.
|
|
37
|
+
*
|
|
38
|
+
* Returns the path *and* its provenance, so every caller — the command context,
|
|
39
|
+
* the auth wiring, the pre-parse plugin bootstrap and `connections list` — reads
|
|
40
|
+
* the same answer instead of re-deriving the chain.
|
|
41
|
+
*/
|
|
42
|
+
export declare function resolveConfigSource(input?: ResolveConfigSourceInput): ConfigSource;
|
|
43
|
+
/** One row of `connections list`: a connection's id and either its base URL or its error. */
|
|
44
|
+
export interface ConnectionEntry {
|
|
45
|
+
id: string;
|
|
46
|
+
baseUrl?: string;
|
|
47
|
+
error?: string;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Every `.js` file in `dir`, by basename, sorted, with its `site.base_url`. A
|
|
51
|
+
* file that cannot be imported, or that declares no usable `site`, is a row
|
|
52
|
+
* carrying its error rather than an abort: the listing reports exactly what is
|
|
53
|
+
* on disk. Plugins are never constructed, so one connection naming an
|
|
54
|
+
* uninstalled package cannot take the others down with it.
|
|
55
|
+
*/
|
|
56
|
+
export declare function listConnections(dir: string): Promise<ConnectionEntry[]>;
|
|
57
|
+
/** Sorted ids of the `.js` files in `dir`; `[]` when the directory is absent. */
|
|
58
|
+
export declare function listConnectionIds(dir: string): Promise<string[]>;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
2
|
+
import { readdir } from "node:fs/promises";
|
|
3
|
+
import { basename, join } from "node:path";
|
|
4
|
+
import { ConfigError } from "../errors.js";
|
|
5
|
+
import { loadConfigSite } from "./config.js";
|
|
6
|
+
import { defaultConnectionsDir } from "./paths.js";
|
|
7
|
+
/** The name of the config file dropsh falls back to in the current directory. */
|
|
8
|
+
export const CWD_CONFIG_FILE = "dropsh.config.js";
|
|
9
|
+
/**
|
|
10
|
+
* The connections directory: `--connections-dir` > `$DROPSH_CONNECTIONS_DIR` >
|
|
11
|
+
* `~/.config/dropsh/connections`. Resolved independently of which config
|
|
12
|
+
* selector wins, because `connections list` reports the directory either way.
|
|
13
|
+
*/
|
|
14
|
+
export function resolveConnectionsDir(input = {}) {
|
|
15
|
+
const env = input.env ?? process.env;
|
|
16
|
+
return input.flag ?? env.DROPSH_CONNECTIONS_DIR ?? defaultConnectionsDir();
|
|
17
|
+
}
|
|
18
|
+
function connectionPath(dir, id) {
|
|
19
|
+
return join(dir, `${id}.js`);
|
|
20
|
+
}
|
|
21
|
+
function toIds(entries) {
|
|
22
|
+
return entries
|
|
23
|
+
.filter((name) => name.endsWith(".js"))
|
|
24
|
+
.map((name) => basename(name, ".js"))
|
|
25
|
+
.sort();
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Sorted ids of the `.js` files in `dir`; `[]` when the directory is absent.
|
|
29
|
+
* Synchronous because it is only reached on the unknown-id error path, and the
|
|
30
|
+
* resolver itself has to stay synchronous for the commander option wiring.
|
|
31
|
+
*/
|
|
32
|
+
function listConnectionIdsSync(dir) {
|
|
33
|
+
try {
|
|
34
|
+
return toIds(readdirSync(dir));
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Resolve the config file for this invocation, highest selector first:
|
|
42
|
+
* `--config` > `--connection` > `$DROPSH_CONFIG` > `$DROPSH_CONNECTION` >
|
|
43
|
+
* `./dropsh.config.js`. `--config` and `--connection` together is not an error:
|
|
44
|
+
* `--config` names a *file* and that is its stated meaning, so it wins silently.
|
|
45
|
+
*
|
|
46
|
+
* Returns the path *and* its provenance, so every caller — the command context,
|
|
47
|
+
* the auth wiring, the pre-parse plugin bootstrap and `connections list` — reads
|
|
48
|
+
* the same answer instead of re-deriving the chain.
|
|
49
|
+
*/
|
|
50
|
+
export function resolveConfigSource(input = {}) {
|
|
51
|
+
const env = input.env ?? process.env;
|
|
52
|
+
const dir = input.connectionsDir ?? resolveConnectionsDir({ env });
|
|
53
|
+
const cwd = input.cwd ?? process.cwd();
|
|
54
|
+
if (input.config)
|
|
55
|
+
return { path: input.config, source: "flag-config" };
|
|
56
|
+
if (input.connection) {
|
|
57
|
+
return {
|
|
58
|
+
path: requireConnection(dir, input.connection),
|
|
59
|
+
source: "flag-connection",
|
|
60
|
+
id: input.connection,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
if (env.DROPSH_CONFIG)
|
|
64
|
+
return { path: env.DROPSH_CONFIG, source: "env-config" };
|
|
65
|
+
if (env.DROPSH_CONNECTION) {
|
|
66
|
+
const id = env.DROPSH_CONNECTION;
|
|
67
|
+
return { path: requireConnection(dir, id), source: "env-connection", id };
|
|
68
|
+
}
|
|
69
|
+
return { path: join(cwd, CWD_CONFIG_FILE), source: "cwd" };
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* The path of connection `id`, or a `ConfigError` naming the id, the directory
|
|
73
|
+
* searched and the available ids. Nothing prompts and nothing is created: an
|
|
74
|
+
* unattended dispatch must fail rather than hang.
|
|
75
|
+
*/
|
|
76
|
+
function requireConnection(dir, id) {
|
|
77
|
+
const path = connectionPath(dir, id);
|
|
78
|
+
if (existsSync(path))
|
|
79
|
+
return path;
|
|
80
|
+
const available = listConnectionIdsSync(dir);
|
|
81
|
+
const tail = available.length > 0
|
|
82
|
+
? `Available in ${dir}: ${available.join(", ")}.`
|
|
83
|
+
: `no connections found in ${dir}.`;
|
|
84
|
+
throw new ConfigError(`Unknown connection '${id}'. ${tail}`);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Every `.js` file in `dir`, by basename, sorted, with its `site.base_url`. A
|
|
88
|
+
* file that cannot be imported, or that declares no usable `site`, is a row
|
|
89
|
+
* carrying its error rather than an abort: the listing reports exactly what is
|
|
90
|
+
* on disk. Plugins are never constructed, so one connection naming an
|
|
91
|
+
* uninstalled package cannot take the others down with it.
|
|
92
|
+
*/
|
|
93
|
+
export async function listConnections(dir) {
|
|
94
|
+
const ids = await listConnectionIds(dir);
|
|
95
|
+
return Promise.all(ids.map(async (id) => {
|
|
96
|
+
try {
|
|
97
|
+
const site = await loadConfigSite(connectionPath(dir, id));
|
|
98
|
+
return { id, baseUrl: site.base_url };
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
return { id, error: err instanceof Error ? err.message : String(err) };
|
|
102
|
+
}
|
|
103
|
+
}));
|
|
104
|
+
}
|
|
105
|
+
/** Sorted ids of the `.js` files in `dir`; `[]` when the directory is absent. */
|
|
106
|
+
export async function listConnectionIds(dir) {
|
|
107
|
+
let entries;
|
|
108
|
+
try {
|
|
109
|
+
entries = await readdir(dir);
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
return [];
|
|
113
|
+
}
|
|
114
|
+
return toIds(entries);
|
|
115
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The user-global dropsh state directory. Single source of that path: the
|
|
3
|
+
* per-host auth store and the connections directory both hang off it, so the
|
|
4
|
+
* two cannot drift onto different roots.
|
|
5
|
+
*/
|
|
6
|
+
export declare function defaultStateDir(): string;
|
|
7
|
+
/** Default location of the named-connection files, one `<id>.js` per connection. */
|
|
8
|
+
export declare function defaultConnectionsDir(): string;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
/**
|
|
4
|
+
* The user-global dropsh state directory. Single source of that path: the
|
|
5
|
+
* per-host auth store and the connections directory both hang off it, so the
|
|
6
|
+
* two cannot drift onto different roots.
|
|
7
|
+
*/
|
|
8
|
+
export function defaultStateDir() {
|
|
9
|
+
return join(homedir(), ".config", "dropsh");
|
|
10
|
+
}
|
|
11
|
+
/** Default location of the named-connection files, one `<id>.js` per connection. */
|
|
12
|
+
export function defaultConnectionsDir() {
|
|
13
|
+
return join(defaultStateDir(), "connections");
|
|
14
|
+
}
|
package/dist/src/index.d.ts
CHANGED
|
@@ -39,4 +39,16 @@ export interface AuthStatusDeps {
|
|
|
39
39
|
}
|
|
40
40
|
export declare function authStatus(deps: AuthStatusDeps): Promise<AuthStatusInfo>;
|
|
41
41
|
export declare function buildProgram(opts?: ProgramOptions): Command;
|
|
42
|
+
/**
|
|
43
|
+
* The config selectors as they appear in raw argv, for the pre-parse plugin
|
|
44
|
+
* bootstrap — which runs before commander sees any option. It feeds the same
|
|
45
|
+
* resolver the commands use, so the early and the late resolution cannot answer
|
|
46
|
+
* differently and load plugins from a different connection than the command
|
|
47
|
+
* being bootstrapped.
|
|
48
|
+
*/
|
|
49
|
+
export declare function earlyConfigArgs(argv: string[]): {
|
|
50
|
+
config?: string;
|
|
51
|
+
connection?: string;
|
|
52
|
+
connectionsDir?: string;
|
|
53
|
+
};
|
|
42
54
|
export declare function main(): Promise<void>;
|
package/dist/src/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import { parseArgs } from "node:util";
|
|
|
3
3
|
import { Command } from "commander";
|
|
4
4
|
import { readDataArg } from "./commands/_data.js";
|
|
5
5
|
import { runAuthLogin, runAuthLogout, runAuthStatus, runAuthUse } from "./commands/auth.js";
|
|
6
|
+
import { renderConnectionsTable, runConnectionsList, } from "./commands/connections.js";
|
|
6
7
|
import { runCreate } from "./commands/create.js";
|
|
7
8
|
import { runDelete } from "./commands/delete.js";
|
|
8
9
|
import { runRead } from "./commands/read.js";
|
|
@@ -16,6 +17,7 @@ import { createFileStore } from "./core/cache/file-store.js";
|
|
|
16
17
|
import { createOutput } from "./core/cli/output.js";
|
|
17
18
|
import { createPrompt } from "./core/cli/prompt.js";
|
|
18
19
|
import { loadConfig } from "./core/config.js";
|
|
20
|
+
import { listConnections, resolveConfigSource, resolveConnectionsDir, } from "./core/connections.js";
|
|
19
21
|
import { createHttpClient } from "./core/http.js";
|
|
20
22
|
import { createJsonApiClient } from "./core/jsonapi/client.js";
|
|
21
23
|
import { buildPayloadFromParameters } from "./core/params/from-parameters.js";
|
|
@@ -26,9 +28,6 @@ import { fetchJsonSchema } from "./core/schema/jsonschema-source.js";
|
|
|
26
28
|
import { toOperationVariant } from "./core/schema/to-jsonschema.js";
|
|
27
29
|
import { validatePayload } from "./core/schema/validate.js";
|
|
28
30
|
import { AuthError, ConfigError, exitCodeFor, ValidationError } from "./errors.js";
|
|
29
|
-
function resolveConfigPath(override) {
|
|
30
|
-
return override ?? process.env.DROPSH_CONFIG ?? "dropsh.config.js";
|
|
31
|
-
}
|
|
32
31
|
// Normalizes a variadic --include option into a flat, trimmed list of field
|
|
33
32
|
// names: splits any comma-containing entries (so both `--include a,b` and
|
|
34
33
|
// `--include a b` work) and drops empty strings.
|
|
@@ -153,7 +152,9 @@ export function buildProgram(opts = {}) {
|
|
|
153
152
|
.name("dropsh")
|
|
154
153
|
.description("Entity-agnostic CLI for Drupal 11 JSON:API")
|
|
155
154
|
.version("0.0.0")
|
|
156
|
-
.option("--config <path>", "path to config file (overrides
|
|
155
|
+
.option("--config <path>", "path to config file (overrides every id-based selector)")
|
|
156
|
+
.option("--connection <id>", "named connection to use: <connections-dir>/<id>.js (overrides $DROPSH_CONFIG)")
|
|
157
|
+
.option("--connections-dir <path>", "where named connections live (overrides $DROPSH_CONNECTIONS_DIR)")
|
|
157
158
|
.option("--auth-profile <id>", "auth profile to use (overrides the active profile and $DROPSH_AUTH_PROFILE)")
|
|
158
159
|
.option("--format <id>", "output format: json (default) or a renderer id", "json")
|
|
159
160
|
.option("--view-mode <name>", "entity view mode for interactive formats", "default");
|
|
@@ -163,11 +164,28 @@ export function buildProgram(opts = {}) {
|
|
|
163
164
|
((c) => {
|
|
164
165
|
process.exitCode = c;
|
|
165
166
|
});
|
|
167
|
+
/** The connections directory for this invocation: `--connections-dir` > env > default. */
|
|
168
|
+
function currentConnectionsDir() {
|
|
169
|
+
return resolveConnectionsDir({ flag: program.opts().connectionsDir });
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* The config file this invocation resolves to, and why. Every caller — the
|
|
173
|
+
* command context, the auth wiring, and `connections list` — reads this one
|
|
174
|
+
* answer, so what the listing claims and what the next command does cannot
|
|
175
|
+
* disagree.
|
|
176
|
+
*/
|
|
177
|
+
function currentConfigSource() {
|
|
178
|
+
const o = program.opts();
|
|
179
|
+
return resolveConfigSource({
|
|
180
|
+
config: o.config,
|
|
181
|
+
connection: o.connection,
|
|
182
|
+
connectionsDir: currentConnectionsDir(),
|
|
183
|
+
});
|
|
184
|
+
}
|
|
166
185
|
const contextFactory = opts.contextFactory ??
|
|
167
186
|
(() => {
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
return defaultContext(resolveConfigPath(o.config), profile);
|
|
187
|
+
const profile = program.opts().authProfile ?? process.env.DROPSH_AUTH_PROFILE;
|
|
188
|
+
return defaultContext(currentConfigSource().path, profile);
|
|
171
189
|
});
|
|
172
190
|
const renderers = (opts.plugins ?? []).flatMap((p) => p.renderers ?? []);
|
|
173
191
|
const output = createOutput({
|
|
@@ -279,7 +297,7 @@ export function buildProgram(opts = {}) {
|
|
|
279
297
|
}
|
|
280
298
|
}
|
|
281
299
|
async function authDeps() {
|
|
282
|
-
const cfg = await loadConfig(
|
|
300
|
+
const cfg = await loadConfig(currentConfigSource().path);
|
|
283
301
|
return {
|
|
284
302
|
baseUrl: cfg.site.base_url,
|
|
285
303
|
providers: collectProviders(cfg.plugins),
|
|
@@ -528,6 +546,27 @@ Example:
|
|
|
528
546
|
});
|
|
529
547
|
}, () => assertJsonOnly("schema"));
|
|
530
548
|
});
|
|
549
|
+
const connections = program.command("connections").description("Manage named connections");
|
|
550
|
+
connections
|
|
551
|
+
.command("list")
|
|
552
|
+
.description("List the connections in the resolved connections directory")
|
|
553
|
+
.action(() =>
|
|
554
|
+
// Builds no CommandContext, so it works with no cwd config present.
|
|
555
|
+
run2(async () => {
|
|
556
|
+
await runConnectionsList({ dir: currentConnectionsDir(), resolved: currentConfigSource() }, {
|
|
557
|
+
list: listConnections,
|
|
558
|
+
// A table because a human asked what the connections are; an
|
|
559
|
+
// explicit --format json hands back the same data verbatim.
|
|
560
|
+
emit: (v) => {
|
|
561
|
+
if (program.getOptionValueSource("format") === "default") {
|
|
562
|
+
stdout(`${renderConnectionsTable(v)}\n`);
|
|
563
|
+
}
|
|
564
|
+
else {
|
|
565
|
+
output.emit(v);
|
|
566
|
+
}
|
|
567
|
+
},
|
|
568
|
+
});
|
|
569
|
+
}));
|
|
531
570
|
const auth = program.command("auth").description("Manage authentication");
|
|
532
571
|
auth
|
|
533
572
|
.command("login")
|
|
@@ -558,17 +597,49 @@ Example:
|
|
|
558
597
|
program.exitOverride();
|
|
559
598
|
return program;
|
|
560
599
|
}
|
|
561
|
-
|
|
600
|
+
/**
|
|
601
|
+
* The config selectors as they appear in raw argv, for the pre-parse plugin
|
|
602
|
+
* bootstrap — which runs before commander sees any option. It feeds the same
|
|
603
|
+
* resolver the commands use, so the early and the late resolution cannot answer
|
|
604
|
+
* differently and load plugins from a different connection than the command
|
|
605
|
+
* being bootstrapped.
|
|
606
|
+
*/
|
|
607
|
+
export function earlyConfigArgs(argv) {
|
|
562
608
|
const { values } = parseArgs({
|
|
563
609
|
args: argv.slice(2),
|
|
564
|
-
options: {
|
|
610
|
+
options: {
|
|
611
|
+
config: { type: "string" },
|
|
612
|
+
connection: { type: "string" },
|
|
613
|
+
"connections-dir": { type: "string" },
|
|
614
|
+
},
|
|
565
615
|
strict: false,
|
|
566
616
|
});
|
|
567
|
-
|
|
617
|
+
const str = (v) => (typeof v === "string" ? v : undefined);
|
|
618
|
+
const config = str(values.config);
|
|
619
|
+
const connection = str(values.connection);
|
|
620
|
+
const connectionsDir = str(values["connections-dir"]);
|
|
621
|
+
return {
|
|
622
|
+
...(config !== undefined ? { config } : {}),
|
|
623
|
+
...(connection !== undefined ? { connection } : {}),
|
|
624
|
+
...(connectionsDir !== undefined ? { connectionsDir } : {}),
|
|
625
|
+
};
|
|
568
626
|
}
|
|
569
627
|
export async function main() {
|
|
570
|
-
const
|
|
571
|
-
|
|
628
|
+
const early = earlyConfigArgs(process.argv);
|
|
629
|
+
// An unresolvable selector is not reported here: the bootstrap only collects
|
|
630
|
+
// plugins, and commander's own resolution raises the error with its exit code.
|
|
631
|
+
const configPath = (() => {
|
|
632
|
+
try {
|
|
633
|
+
return resolveConfigSource({
|
|
634
|
+
...early,
|
|
635
|
+
connectionsDir: resolveConnectionsDir({ flag: early.connectionsDir }),
|
|
636
|
+
}).path;
|
|
637
|
+
}
|
|
638
|
+
catch {
|
|
639
|
+
return null;
|
|
640
|
+
}
|
|
641
|
+
})();
|
|
642
|
+
const cfg = configPath === null ? null : await loadConfig(configPath).catch(() => null);
|
|
572
643
|
const plugins = cfg?.plugins ?? [];
|
|
573
644
|
const program = buildProgram({ plugins });
|
|
574
645
|
try {
|
package/dist/src/plugin-api.d.ts
CHANGED
|
@@ -5,6 +5,8 @@ export type { AnyRenderer, InteractiveRenderer, RenderContext, Renderer, RenderS
|
|
|
5
5
|
export { indexIncluded, isInteractive } from "./core/cli/render.js";
|
|
6
6
|
export type { Config, SiteConfig } from "./core/config.js";
|
|
7
7
|
export { loadConfig } from "./core/config.js";
|
|
8
|
+
export type { ConfigSource, ConfigSourceKind, ConnectionEntry, ResolveConfigSourceInput, ResolveConnectionsDirInput, } from "./core/connections.js";
|
|
9
|
+
export { CWD_CONFIG_FILE, listConnectionIds, listConnections, resolveConfigSource, resolveConnectionsDir, } from "./core/connections.js";
|
|
8
10
|
export type { HttpClient, HttpRequest } from "./core/http.js";
|
|
9
11
|
export { createHttpClient } from "./core/http.js";
|
|
10
12
|
export type { DropSHOperation, JsonApiClient, JsonApiOptions, ResourceWriteBody, } from "./core/jsonapi/client.js";
|
package/dist/src/plugin-api.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { basicAuthPlugin, basicAuthProvider } from "./core/auth/basic.js";
|
|
2
2
|
export { indexIncluded, isInteractive } from "./core/cli/render.js";
|
|
3
3
|
export { loadConfig } from "./core/config.js";
|
|
4
|
+
export { CWD_CONFIG_FILE, listConnectionIds, listConnections, resolveConfigSource, resolveConnectionsDir, } from "./core/connections.js";
|
|
4
5
|
export { createHttpClient } from "./core/http.js";
|
|
5
6
|
export { createJsonApiClient } from "./core/jsonapi/client.js";
|
|
6
7
|
export { buildPayloadFromParameters } from "./core/params/from-parameters.js";
|