dropsh 0.5.1 → 0.5.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/README.md +170 -25
- package/dist/src/commands/create.js +2 -2
- package/dist/src/commands/read.js +11 -2
- package/dist/src/commands/search.js +3 -1
- package/dist/src/commands/update.js +2 -2
- package/dist/src/core/cli/output.js +32 -3
- package/dist/src/core/cli/render.js +13 -0
- package/dist/src/core/context.js +44 -0
- package/dist/src/index.js +87 -16
- package/dist/src/plugin-api.js +1 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -28,8 +28,27 @@ content without hard-coding every content model.
|
|
|
28
28
|
|
|
29
29
|
## Install
|
|
30
30
|
|
|
31
|
+
Install dropsh globally to put the `dropsh` binary on `PATH`. No local project,
|
|
32
|
+
`package.json`, or `node_modules` is required — dropsh runs from anywhere:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npm i -g dropsh # or: pnpm add -g dropsh
|
|
36
|
+
dropsh --help # binary now on PATH, no project needed
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
dropsh needs Node.js 20 or newer.
|
|
40
|
+
|
|
41
|
+
Plugins are **separate published packages** and are **not** bundled in the
|
|
42
|
+
`dropsh` tarball. To use one, install it next to dropsh — either globally
|
|
43
|
+
(`npm i -g dropsh @dropsh/plugin-oauth2`) or for a single command via
|
|
44
|
+
`npx -p dropsh -p @dropsh/plugin-oauth2 dropsh …`. See [Plugins](#plugins) for
|
|
45
|
+
how names resolve.
|
|
46
|
+
|
|
47
|
+
<details>
|
|
48
|
+
<summary>From source (contributors)</summary>
|
|
49
|
+
|
|
31
50
|
This repository is a pnpm workspace containing the root CLI, packages, and
|
|
32
|
-
plugins.
|
|
51
|
+
plugins. Build it from source when developing dropsh itself:
|
|
33
52
|
|
|
34
53
|
```bash
|
|
35
54
|
corepack enable
|
|
@@ -38,6 +57,11 @@ pnpm run build
|
|
|
38
57
|
```
|
|
39
58
|
|
|
40
59
|
The required Node.js and pinned pnpm versions are declared in `package.json`.
|
|
60
|
+
Inside the workspace, plugins resolve from the local `node_modules`, so config
|
|
61
|
+
may import the plugin factories directly (see the workspace variant under
|
|
62
|
+
[Configuration](#configuration)).
|
|
63
|
+
|
|
64
|
+
</details>
|
|
41
65
|
|
|
42
66
|
## Drupal Prerequisites
|
|
43
67
|
|
|
@@ -104,16 +128,17 @@ delete operations** at `/admin/config/services/jsonapi` (or via
|
|
|
104
128
|
|
|
105
129
|
## Configuration
|
|
106
130
|
|
|
107
|
-
Create a `dropsh.config.js` in the
|
|
108
|
-
only non-secret connection settings. Passwords, client secrets, and tokens
|
|
109
|
-
prompted during login and stored outside the project in the per-host session
|
|
131
|
+
Create a `dropsh.config.js` in the directory where you run dropsh. The config
|
|
132
|
+
holds only non-secret connection settings. Passwords, client secrets, and tokens
|
|
133
|
+
are prompted during login and stored outside the project in the per-host session
|
|
110
134
|
store.
|
|
111
135
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
import
|
|
115
|
-
|
|
136
|
+
Register plugins as **descriptor objects** — `{ plugin, with?, export? }` — that
|
|
137
|
+
name a package by string. dropsh resolves and constructs each plugin itself, so
|
|
138
|
+
the config needs **no `import` statements** and **no local `node_modules`**. This
|
|
139
|
+
is the path a global install uses:
|
|
116
140
|
|
|
141
|
+
```js
|
|
117
142
|
export default {
|
|
118
143
|
site: {
|
|
119
144
|
base_url: "https://my-drupal.example.com",
|
|
@@ -123,6 +148,45 @@ export default {
|
|
|
123
148
|
dry_run: false,
|
|
124
149
|
timeout_ms: 30000,
|
|
125
150
|
},
|
|
151
|
+
plugins: [
|
|
152
|
+
{ plugin: "dropsh/plugin", export: "basicAuthPlugin" },
|
|
153
|
+
{
|
|
154
|
+
plugin: "@dropsh/plugin-oauth2",
|
|
155
|
+
export: "oauth2Plugin",
|
|
156
|
+
with: {
|
|
157
|
+
type: "oauth2_authcode",
|
|
158
|
+
client_id: "my-client",
|
|
159
|
+
token_url: "https://my-drupal.example.com/oauth/token",
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
{ plugin: "@dropsh/plugin-jsonapi-schema", export: "jsonapiSchemaPlugin" },
|
|
163
|
+
],
|
|
164
|
+
};
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Each descriptor's `with` is passed to the plugin factory (legacy alias:
|
|
168
|
+
`options`). `export` names the factory to call and is **required** for every
|
|
169
|
+
`@dropsh/plugin-*` package — they export named factories with no default export
|
|
170
|
+
(see [Plugins](#plugins)). Basic auth lives on the `dropsh/plugin` entry, so its
|
|
171
|
+
descriptor names `dropsh/plugin` with `export: "basicAuthPlugin"`.
|
|
172
|
+
|
|
173
|
+
Use `dropsh.config.example.js` as a fuller starting point. Override the config
|
|
174
|
+
path with `--config <path>` or `DROPSH_CONFIG`.
|
|
175
|
+
|
|
176
|
+
<details>
|
|
177
|
+
<summary>Workspace variant — importing factories directly</summary>
|
|
178
|
+
|
|
179
|
+
Inside the pnpm workspace or any project that has the plugin packages in its
|
|
180
|
+
local `node_modules`, you may skip descriptors and import the factories instead.
|
|
181
|
+
The `plugins[]` array then holds constructed plugins:
|
|
182
|
+
|
|
183
|
+
```js
|
|
184
|
+
import { basicAuthPlugin } from "dropsh/plugin";
|
|
185
|
+
import { oauth2Plugin } from "@dropsh/plugin-oauth2";
|
|
186
|
+
import { jsonapiSchemaPlugin } from "@dropsh/plugin-jsonapi-schema";
|
|
187
|
+
|
|
188
|
+
export default {
|
|
189
|
+
site: { base_url: "https://my-drupal.example.com", jsonapi_prefix: "/jsonapi" },
|
|
126
190
|
plugins: [
|
|
127
191
|
basicAuthPlugin(),
|
|
128
192
|
oauth2Plugin({
|
|
@@ -135,8 +199,10 @@ export default {
|
|
|
135
199
|
};
|
|
136
200
|
```
|
|
137
201
|
|
|
138
|
-
|
|
139
|
-
|
|
202
|
+
The two forms may be mixed in one `plugins[]` array. `import` resolves relative
|
|
203
|
+
to the config file, so it only works when the packages are installed there.
|
|
204
|
+
|
|
205
|
+
</details>
|
|
140
206
|
|
|
141
207
|
## Authentication
|
|
142
208
|
|
|
@@ -227,12 +293,12 @@ All commands write JSON to stdout, structured errors to stderr, and use stable
|
|
|
227
293
|
exit codes.
|
|
228
294
|
|
|
229
295
|
```bash
|
|
230
|
-
dropsh read <entity_type>/<bundle>/<uuid>
|
|
231
|
-
dropsh search <entity_type> [--bundle=<
|
|
232
|
-
dropsh create <entity_type> --bundle=<
|
|
233
|
-
dropsh update <entity_type>/<bundle>/<uuid> --data=<json|@file> [--dry-run]
|
|
296
|
+
dropsh read <entity_type>/<bundle>/<uuid> [--include=<field>,…]
|
|
297
|
+
dropsh search <entity_type> [--bundle=<b>] [--filter=key:value]… [--limit=N] [--include=<field>,…]
|
|
298
|
+
dropsh create <entity_type> --bundle=<b> --data=<json|@file> [--dry-run] [--no-validate]
|
|
299
|
+
dropsh update <entity_type>/<bundle>/<uuid> --data=<json|@file> [--dry-run] [--no-validate]
|
|
234
300
|
dropsh delete <entity_type>/<bundle>/<uuid> [--dry-run]
|
|
235
|
-
dropsh upload-file --target=<entity_type>/<bundle>/<uuid>/<field> --file=<path>
|
|
301
|
+
dropsh upload-file --target=<entity_type>/<bundle>/<uuid>/<field> --file=<path> [--dry-run]
|
|
236
302
|
dropsh schema [--refresh]
|
|
237
303
|
dropsh schema <entity_type>/<bundle> [--for=create|update] [--refresh]
|
|
238
304
|
```
|
|
@@ -240,6 +306,49 @@ dropsh schema <entity_type>/<bundle> [--for=create|update] [--refresh]
|
|
|
240
306
|
`create` and `update` validate payloads against the current schema by default.
|
|
241
307
|
Pass `--no-validate` when you intentionally want to skip local validation.
|
|
242
308
|
|
|
309
|
+
### `--include` on `read` / `search`
|
|
310
|
+
|
|
311
|
+
Pass `--include` to embed related resources in the response's JSON:API
|
|
312
|
+
`included` array (the standard `?include=` query parameter). Accepts a
|
|
313
|
+
comma-separated list or repeated/space-separated values, and nested paths with
|
|
314
|
+
dots:
|
|
315
|
+
|
|
316
|
+
```bash
|
|
317
|
+
dropsh read node/article/<uuid> --include field_related,field_image
|
|
318
|
+
dropsh search node --bundle=article --include field_related --filter status:1
|
|
319
|
+
dropsh read node/article/<uuid> --include field_related.uid
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
`read` and `search` are the only commands that accept `--include`. Without it,
|
|
323
|
+
the response contains only the primary resource(s).
|
|
324
|
+
|
|
325
|
+
### Output formats
|
|
326
|
+
|
|
327
|
+
By default every command emits JSON. Load a renderer plugin and pass a global
|
|
328
|
+
`--format <id>` (before the subcommand) to render entities differently:
|
|
329
|
+
|
|
330
|
+
```bash
|
|
331
|
+
dropsh --format md read node/article/<uuid> # Markdown detail view
|
|
332
|
+
dropsh --format table search node --bundle=article # aligned table
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
`--format` applies only to entity commands (`read`, `search`, `create`,
|
|
336
|
+
`update`). Using it on `delete`, `upload-file`, or `schema` exits with code 2.
|
|
337
|
+
|
|
338
|
+
The `@dropsh/plugin-tui` plugin adds an interactive full-screen format,
|
|
339
|
+
selected the same way via `--format tui` (requires a TTY — an interactive
|
|
340
|
+
terminal):
|
|
341
|
+
|
|
342
|
+
```bash
|
|
343
|
+
dropsh --format tui read node/article/<uuid> # opens straight into the detail pane
|
|
344
|
+
dropsh --format tui search node --bundle=article # list → Enter → detail pane
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
`read --format tui` boots directly into the detail pane for the single
|
|
348
|
+
entity. `search --format tui` shows a navigable list (↑/↓, Enter opens the
|
|
349
|
+
detail pane, Esc/q goes back or quits). `--include` flows through to the
|
|
350
|
+
detail pane in both cases.
|
|
351
|
+
|
|
243
352
|
## Plugins
|
|
244
353
|
|
|
245
354
|
Plugins are regular JavaScript or TypeScript modules that return a `dropshPlugin`.
|
|
@@ -257,14 +366,47 @@ structures, allowed values, and other metadata that helps the agent produce a
|
|
|
257
366
|
valid payload. Auth plugins keep the same command interface while supporting
|
|
258
367
|
common Drupal authentication systems such as basic auth or OAuth2.
|
|
259
368
|
|
|
260
|
-
|
|
369
|
+
Plugins developed in this repository (**"bundled"** = part of this repo, **not**
|
|
370
|
+
shipped inside the `dropsh` npm tarball — each is a separately published package):
|
|
261
371
|
|
|
262
|
-
| Package | Purpose |
|
|
263
|
-
| --- | --- |
|
|
264
|
-
| [`@dropsh/plugin-oauth2`](plugins/oauth2/README.md) | OAuth2 auth provider for Drupal `simple_oauth` |
|
|
265
|
-
| [`@dropsh/plugin-
|
|
266
|
-
| [`@dropsh/plugin-
|
|
267
|
-
| [`@dropsh/plugin-
|
|
372
|
+
| Package | Factory export | Purpose |
|
|
373
|
+
| --- | --- | --- |
|
|
374
|
+
| [`@dropsh/plugin-oauth2`](plugins/oauth2/README.md) | `oauth2Plugin` | OAuth2 auth provider for Drupal `simple_oauth` |
|
|
375
|
+
| [`@dropsh/plugin-jsonapi-schema`](plugins/jsonapi-schema/README.md) | `jsonapiSchemaPlugin` | authoritative JSON Schema from Drupal `jsonapi_schema` |
|
|
376
|
+
| [`@dropsh/plugin-schemata`](plugins/schemata/README.md) | `schemataPlugin` | authoritative JSON Schema from Drupal `schemata` (legacy) |
|
|
377
|
+
| [`@dropsh/plugin-canvas`](plugins/canvas/README.md) | `canvasPlugin` | Canvas component schemas from `jsonapi_sdc` |
|
|
378
|
+
| [`@dropsh/plugin-display-builder`](plugins/display-builder/README.md) | `displayBuilderPlugin` | Display Builder operation schemas and metadata |
|
|
379
|
+
|
|
380
|
+
Basic auth is not a separate package — its `basicAuthPlugin` factory ships in the
|
|
381
|
+
core CLI's `dropsh/plugin` entry.
|
|
382
|
+
|
|
383
|
+
### Installing and resolving plugins
|
|
384
|
+
|
|
385
|
+
`@dropsh/plugin-*` packages are **not** dependencies of `dropsh` and are **not**
|
|
386
|
+
part of its tarball, so a global dropsh does not carry them. Install the ones you
|
|
387
|
+
use next to dropsh:
|
|
388
|
+
|
|
389
|
+
```bash
|
|
390
|
+
npm i -g dropsh @dropsh/plugin-oauth2 # globally, alongside a global dropsh
|
|
391
|
+
npx -p dropsh -p @dropsh/plugin-oauth2 dropsh … # or one-off, no global install
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
A descriptor's `plugin` name is resolved like ESLint resolves its plugins, by
|
|
395
|
+
string name against these locations **in order**:
|
|
396
|
+
|
|
397
|
+
1. the directory of the `dropsh.config.js` file
|
|
398
|
+
2. the current working directory
|
|
399
|
+
3. dropsh's own install location
|
|
400
|
+
|
|
401
|
+
Location 3 is what lets a plugin installed globally next to a global `dropsh` be
|
|
402
|
+
found with no local project. If the name cannot be resolved from any of them,
|
|
403
|
+
dropsh reports which package to install.
|
|
404
|
+
|
|
405
|
+
`export` selects the factory to call and defaults to `"default"`. Every
|
|
406
|
+
`@dropsh/plugin-*` factory is a **named** export with **no default**, so a
|
|
407
|
+
descriptor for one of these packages **must** set `export` (e.g.
|
|
408
|
+
`export: "oauth2Plugin"` — see the table above). Basic auth uses
|
|
409
|
+
`{ plugin: "dropsh/plugin", export: "basicAuthPlugin" }`.
|
|
268
410
|
|
|
269
411
|
Plugin APIs are exported from `dropsh/plugin`:
|
|
270
412
|
|
|
@@ -282,15 +424,18 @@ export function examplePlugin(): dropshPlugin {
|
|
|
282
424
|
}
|
|
283
425
|
```
|
|
284
426
|
|
|
285
|
-
Register
|
|
427
|
+
Register a package plugin with a descriptor (no import needed), and a local
|
|
428
|
+
plugin either by descriptor (path name) or by importing it:
|
|
286
429
|
|
|
287
430
|
```js
|
|
288
|
-
import { basicAuthPlugin } from "dropsh";
|
|
289
431
|
import { examplePlugin } from "./plugins/example.js";
|
|
290
432
|
|
|
291
433
|
export default {
|
|
292
434
|
site: { base_url: "https://my-drupal.example.com", jsonapi_prefix: "/jsonapi" },
|
|
293
|
-
plugins: [
|
|
435
|
+
plugins: [
|
|
436
|
+
{ plugin: "dropsh/plugin", export: "basicAuthPlugin" }, // packaged, resolved by name
|
|
437
|
+
examplePlugin(), // local module, imported directly
|
|
438
|
+
],
|
|
294
439
|
};
|
|
295
440
|
```
|
|
296
441
|
|
|
@@ -6,9 +6,9 @@ export async function runCreate(args, deps) {
|
|
|
6
6
|
await deps.validate(payload, target);
|
|
7
7
|
}
|
|
8
8
|
if (args.dryRun) {
|
|
9
|
-
deps.emit({ dry_run: true, method: "POST", path: target, payload });
|
|
9
|
+
await deps.emit({ dry_run: true, method: "POST", path: target, payload });
|
|
10
10
|
return;
|
|
11
11
|
}
|
|
12
12
|
const res = await deps.client.post(target, payload);
|
|
13
|
-
deps.emit(res);
|
|
13
|
+
await deps.emit(res);
|
|
14
14
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { DrupalJsonApiParams } from "drupal-jsonapi-params";
|
|
1
2
|
import { ValidationError } from "../errors.js";
|
|
2
3
|
// Allow short test UUIDs (e.g. "u1") and full Drupal UUIDs (36 chars)
|
|
3
4
|
const TARGET_RE = /^[a-z0-9_]+\/[a-z0-9_]+\/[a-z0-9_-]+$/;
|
|
@@ -5,6 +6,14 @@ export async function runRead(args, deps) {
|
|
|
5
6
|
if (!TARGET_RE.test(args.target)) {
|
|
6
7
|
throw new ValidationError(`target must be <entity_type>/<bundle>/<uuid>, got "${args.target}"`);
|
|
7
8
|
}
|
|
8
|
-
|
|
9
|
-
|
|
9
|
+
let res;
|
|
10
|
+
if (args.include?.length) {
|
|
11
|
+
const params = new DrupalJsonApiParams();
|
|
12
|
+
params.addInclude(args.include);
|
|
13
|
+
res = await deps.client.get(args.target, params);
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
res = await deps.client.get(args.target);
|
|
17
|
+
}
|
|
18
|
+
await deps.emit(res);
|
|
10
19
|
}
|
|
@@ -24,7 +24,9 @@ export async function runSearch(args, deps) {
|
|
|
24
24
|
params.addFilter(f.key, f.value);
|
|
25
25
|
}
|
|
26
26
|
params.addPageLimit(args.limit);
|
|
27
|
+
if (args.include?.length)
|
|
28
|
+
params.addInclude(args.include);
|
|
27
29
|
const path = args.bundle ? `${args.entityType}/${args.bundle}` : args.entityType;
|
|
28
30
|
const res = await deps.client.get(path, params);
|
|
29
|
-
deps.emit(res);
|
|
31
|
+
await deps.emit(res);
|
|
30
32
|
}
|
|
@@ -12,9 +12,9 @@ export async function runUpdate(args, deps) {
|
|
|
12
12
|
await deps.validate(payload, schemaTarget);
|
|
13
13
|
}
|
|
14
14
|
if (args.dryRun) {
|
|
15
|
-
deps.emit({ dry_run: true, method: "PATCH", path: args.target, payload });
|
|
15
|
+
await deps.emit({ dry_run: true, method: "PATCH", path: args.target, payload });
|
|
16
16
|
return;
|
|
17
17
|
}
|
|
18
18
|
const res = await deps.client.patch(args.target, payload);
|
|
19
|
-
deps.emit(res);
|
|
19
|
+
await deps.emit(res);
|
|
20
20
|
}
|
|
@@ -1,8 +1,34 @@
|
|
|
1
|
-
import { CliError } from "../../errors.js";
|
|
1
|
+
import { CliError, ConfigError } from "../../errors.js";
|
|
2
|
+
import { isInteractive, } from "./render.js";
|
|
3
|
+
function isJsonApiDocument(v) {
|
|
4
|
+
return !!v && typeof v === "object" && "data" in v;
|
|
5
|
+
}
|
|
2
6
|
export function createOutput(opts) {
|
|
7
|
+
const registry = new Map();
|
|
8
|
+
for (const r of opts.renderers ?? []) {
|
|
9
|
+
if (registry.has(r.id))
|
|
10
|
+
throw new ConfigError(`Duplicate renderer id: ${r.id}`);
|
|
11
|
+
registry.set(r.id, r);
|
|
12
|
+
}
|
|
13
|
+
const getFormat = opts.getFormat ?? (() => "json");
|
|
3
14
|
return {
|
|
4
|
-
emit(value) {
|
|
5
|
-
|
|
15
|
+
emit(value, ctx, services) {
|
|
16
|
+
const fmt = getFormat();
|
|
17
|
+
if (fmt === "json" || !isJsonApiDocument(value)) {
|
|
18
|
+
opts.stdout(`${JSON.stringify(value)}\n`);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const renderer = registry.get(fmt);
|
|
22
|
+
if (!renderer) {
|
|
23
|
+
throw new ConfigError(`Unknown format: ${fmt}`, {
|
|
24
|
+
available: ["json", ...registry.keys()],
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
if (isInteractive(renderer)) {
|
|
28
|
+
return renderer.run(value, ctx ?? { command: "read" }, services);
|
|
29
|
+
}
|
|
30
|
+
const text = renderer.render(value, ctx ?? { command: "read" });
|
|
31
|
+
opts.stdout(text.endsWith("\n") ? text : `${text}\n`);
|
|
6
32
|
},
|
|
7
33
|
fail(err) {
|
|
8
34
|
const payload = err instanceof CliError
|
|
@@ -16,5 +42,8 @@ export function createOutput(opts) {
|
|
|
16
42
|
};
|
|
17
43
|
opts.stderr(`${JSON.stringify(payload)}\n`);
|
|
18
44
|
},
|
|
45
|
+
hasFormat(id) {
|
|
46
|
+
return id === "json" || registry.has(id);
|
|
47
|
+
},
|
|
19
48
|
};
|
|
20
49
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Indexes a document's `included` resources by `${type}/${id}` so renderers can
|
|
2
|
+
// resolve a relationship reference to its embedded resource (populated by
|
|
3
|
+
// `--include`). Empty map when the document carries no `included`.
|
|
4
|
+
export function indexIncluded(doc) {
|
|
5
|
+
const map = new Map();
|
|
6
|
+
for (const r of doc.included ?? []) {
|
|
7
|
+
map.set(`${r.type}/${r.id}`, r);
|
|
8
|
+
}
|
|
9
|
+
return map;
|
|
10
|
+
}
|
|
11
|
+
export function isInteractive(r) {
|
|
12
|
+
return "interactive" in r && r.interactive === true;
|
|
13
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { AuthError, ConfigError } from "../errors.js";
|
|
2
|
+
import { collectProviders, providerById } from "./auth/registry.js";
|
|
3
|
+
import { readSession, writeSession } from "./auth/session-store.js";
|
|
4
|
+
import { loadConfig } from "./config.js";
|
|
5
|
+
import { createHttpClient } from "./http.js";
|
|
6
|
+
import { createJsonApiClient } from "./jsonapi/client.js";
|
|
7
|
+
export async function resolveAuth(deps) {
|
|
8
|
+
const rec = await readSession(deps.baseUrl, deps.stateDir);
|
|
9
|
+
if (!rec)
|
|
10
|
+
throw new AuthError("Not authenticated. Run 'dropsh auth login'.");
|
|
11
|
+
const provider = providerById(collectProviders(deps.plugins), rec.activeProvider);
|
|
12
|
+
if (!provider)
|
|
13
|
+
throw new ConfigError(`active provider '${rec.activeProvider}' is not configured`);
|
|
14
|
+
return provider.createAdapter(rec.session, {
|
|
15
|
+
http: deps.http,
|
|
16
|
+
now: deps.now,
|
|
17
|
+
save: (session) => writeSession(deps.baseUrl, provider.id, session, deps.stateDir),
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
export async function createCommandContext(configPath) {
|
|
21
|
+
const cfg = await loadConfig(configPath);
|
|
22
|
+
const http = createHttpClient({ timeoutMs: cfg.defaults.timeout_ms });
|
|
23
|
+
const auth = await resolveAuth({
|
|
24
|
+
baseUrl: cfg.site.base_url,
|
|
25
|
+
plugins: cfg.plugins,
|
|
26
|
+
http,
|
|
27
|
+
now: Date.now,
|
|
28
|
+
});
|
|
29
|
+
const client = createJsonApiClient({
|
|
30
|
+
baseUrl: cfg.site.base_url,
|
|
31
|
+
prefix: cfg.site.jsonapi_prefix,
|
|
32
|
+
http,
|
|
33
|
+
auth,
|
|
34
|
+
});
|
|
35
|
+
return {
|
|
36
|
+
client,
|
|
37
|
+
http,
|
|
38
|
+
auth,
|
|
39
|
+
baseUrl: cfg.site.base_url,
|
|
40
|
+
jsonapiPrefix: cfg.site.jsonapi_prefix,
|
|
41
|
+
cwd: process.cwd(),
|
|
42
|
+
plugins: cfg.plugins,
|
|
43
|
+
};
|
|
44
|
+
}
|
package/dist/src/index.js
CHANGED
|
@@ -24,6 +24,17 @@ import { AuthError, ConfigError, exitCodeFor } from "./errors.js";
|
|
|
24
24
|
function resolveConfigPath(override) {
|
|
25
25
|
return override ?? process.env.DROPSH_CONFIG ?? "dropsh.config.js";
|
|
26
26
|
}
|
|
27
|
+
// Normalizes a variadic --include option into a flat, trimmed list of field
|
|
28
|
+
// names: splits any comma-containing entries (so both `--include a,b` and
|
|
29
|
+
// `--include a b` work) and drops empty strings.
|
|
30
|
+
export function normalizeInclude(raw) {
|
|
31
|
+
if (!raw)
|
|
32
|
+
return [];
|
|
33
|
+
return raw
|
|
34
|
+
.flatMap((entry) => entry.split(","))
|
|
35
|
+
.map((s) => s.trim())
|
|
36
|
+
.filter((s) => s.length > 0);
|
|
37
|
+
}
|
|
27
38
|
/**
|
|
28
39
|
* Choose the profile name to authenticate as. Precedence:
|
|
29
40
|
* explicit → stored `active` → config `default:true` → the sole provider.
|
|
@@ -131,7 +142,9 @@ export function buildProgram(opts = {}) {
|
|
|
131
142
|
.description("Entity-agnostic CLI for Drupal 11 JSON:API")
|
|
132
143
|
.version("0.0.0")
|
|
133
144
|
.option("--config <path>", "path to config file (overrides DROPSH_CONFIG)")
|
|
134
|
-
.option("--auth-profile <id>", "auth profile to use (overrides the active profile and $DROPSH_AUTH_PROFILE)")
|
|
145
|
+
.option("--auth-profile <id>", "auth profile to use (overrides the active profile and $DROPSH_AUTH_PROFILE)")
|
|
146
|
+
.option("--format <id>", "output format: json (default) or a renderer id", "json")
|
|
147
|
+
.option("--view-mode <name>", "entity view mode for interactive formats", "default");
|
|
135
148
|
const stdout = opts.stdout ?? ((s) => process.stdout.write(s));
|
|
136
149
|
const stderr = opts.stderr ?? ((s) => process.stderr.write(s));
|
|
137
150
|
const setExitCode = opts.setExitCode ??
|
|
@@ -144,9 +157,16 @@ export function buildProgram(opts = {}) {
|
|
|
144
157
|
const profile = o.authProfile ?? process.env.DROPSH_AUTH_PROFILE;
|
|
145
158
|
return defaultContext(resolveConfigPath(o.config), profile);
|
|
146
159
|
});
|
|
147
|
-
const
|
|
148
|
-
|
|
160
|
+
const renderers = (opts.plugins ?? []).flatMap((p) => p.renderers ?? []);
|
|
161
|
+
const output = createOutput({
|
|
162
|
+
stdout,
|
|
163
|
+
stderr,
|
|
164
|
+
renderers,
|
|
165
|
+
getFormat: () => program.opts().format ?? "json",
|
|
166
|
+
});
|
|
167
|
+
async function run(fn, precheck) {
|
|
149
168
|
try {
|
|
169
|
+
precheck?.();
|
|
150
170
|
const ctx = await contextFactory();
|
|
151
171
|
await fn(ctx);
|
|
152
172
|
}
|
|
@@ -164,6 +184,21 @@ export function buildProgram(opts = {}) {
|
|
|
164
184
|
setExitCode(exitCodeFor(err));
|
|
165
185
|
}
|
|
166
186
|
}
|
|
187
|
+
function currentFormat() {
|
|
188
|
+
return program.opts().format ?? "json";
|
|
189
|
+
}
|
|
190
|
+
function assertRenderable() {
|
|
191
|
+
const f = currentFormat();
|
|
192
|
+
if (!output.hasFormat(f)) {
|
|
193
|
+
throw new ConfigError(`Unknown format '${f}'. Available: ${["json", ...renderers.map((r) => r.id)].join(", ")}`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
function assertJsonOnly(command) {
|
|
197
|
+
const f = currentFormat();
|
|
198
|
+
if (f !== "json") {
|
|
199
|
+
throw new ConfigError(`format '${f}' not applicable to command '${command}'`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
167
202
|
async function authDeps() {
|
|
168
203
|
const cfg = await loadConfig(resolveConfigPath(program.opts().config));
|
|
169
204
|
return {
|
|
@@ -224,19 +259,48 @@ export function buildProgram(opts = {}) {
|
|
|
224
259
|
program
|
|
225
260
|
.command("read <target>")
|
|
226
261
|
.description("Read entity_type/bundle/uuid")
|
|
227
|
-
.
|
|
262
|
+
.option("--include <fields...>", "related fields to include (JSON:API include)")
|
|
263
|
+
.action((target, o) => {
|
|
264
|
+
const [entityType, bundle] = target.split("/");
|
|
265
|
+
const rctx = { command: "read", target };
|
|
266
|
+
if (entityType !== undefined)
|
|
267
|
+
rctx.entityType = entityType;
|
|
268
|
+
if (bundle !== undefined)
|
|
269
|
+
rctx.bundle = bundle;
|
|
270
|
+
rctx.viewMode = program.opts().viewMode;
|
|
271
|
+
// biome-ignore lint/suspicious/noExplicitAny: optional include added conditionally
|
|
272
|
+
const args = { target };
|
|
273
|
+
const include = normalizeInclude(o.include);
|
|
274
|
+
if (include.length > 0)
|
|
275
|
+
args.include = include;
|
|
276
|
+
return run((ctx) => runRead(args, {
|
|
277
|
+
client: ctx.client,
|
|
278
|
+
emit: (v) => output.emit(v, rctx, { client: ctx.client, baseUrl: ctx.baseUrl }),
|
|
279
|
+
}), assertRenderable);
|
|
280
|
+
});
|
|
228
281
|
program
|
|
229
282
|
.command("search <entity_type>")
|
|
230
283
|
.description("Search entities with filters")
|
|
231
284
|
.option("--bundle <bundle>")
|
|
232
285
|
.option("--filter <kv...>", "filter in key:value or key:op:value form", [])
|
|
233
286
|
.option("--limit <n>", "max results", (v) => parseInt(v, 10), 50)
|
|
287
|
+
.option("--include <fields...>", "related fields to include (JSON:API include)")
|
|
234
288
|
.action((entityType, o) => {
|
|
235
|
-
// biome-ignore lint/suspicious/noExplicitAny: optional bundle added conditionally
|
|
289
|
+
// biome-ignore lint/suspicious/noExplicitAny: optional bundle/include added conditionally
|
|
236
290
|
const args = { entityType, filters: o.filter, limit: o.limit };
|
|
237
291
|
if (o.bundle !== undefined)
|
|
238
292
|
args.bundle = o.bundle;
|
|
239
|
-
|
|
293
|
+
const include = normalizeInclude(o.include);
|
|
294
|
+
if (include.length > 0)
|
|
295
|
+
args.include = include;
|
|
296
|
+
const rctx = { command: "search", entityType };
|
|
297
|
+
if (o.bundle !== undefined)
|
|
298
|
+
rctx.bundle = o.bundle;
|
|
299
|
+
rctx.viewMode = program.opts().viewMode;
|
|
300
|
+
return run((ctx) => runSearch(args, {
|
|
301
|
+
client: ctx.client,
|
|
302
|
+
emit: (v) => output.emit(v, rctx, { client: ctx.client, baseUrl: ctx.baseUrl }),
|
|
303
|
+
}), assertRenderable);
|
|
240
304
|
});
|
|
241
305
|
program
|
|
242
306
|
.command("create <entity_type>")
|
|
@@ -251,8 +315,9 @@ export function buildProgram(opts = {}) {
|
|
|
251
315
|
args.dryRun = o.dryRun;
|
|
252
316
|
if (o.validate === false)
|
|
253
317
|
args.noValidate = true;
|
|
254
|
-
run(async (ctx) => {
|
|
255
|
-
const
|
|
318
|
+
return run(async (ctx) => {
|
|
319
|
+
const rctx = { command: "create", entityType, bundle: o.bundle };
|
|
320
|
+
const deps = { client: ctx.client, emit: (v) => output.emit(v, rctx) };
|
|
256
321
|
if (!args.noValidate) {
|
|
257
322
|
deps.validate = async (payload, target) => {
|
|
258
323
|
const schema = await loadOrFetchSchema(ctx, target, "create");
|
|
@@ -260,7 +325,7 @@ export function buildProgram(opts = {}) {
|
|
|
260
325
|
};
|
|
261
326
|
}
|
|
262
327
|
await runCreate(args, deps);
|
|
263
|
-
});
|
|
328
|
+
}, assertRenderable);
|
|
264
329
|
});
|
|
265
330
|
program
|
|
266
331
|
.command("update <target>")
|
|
@@ -274,8 +339,14 @@ export function buildProgram(opts = {}) {
|
|
|
274
339
|
args.dryRun = o.dryRun;
|
|
275
340
|
if (o.validate === false)
|
|
276
341
|
args.noValidate = true;
|
|
277
|
-
run(async (ctx) => {
|
|
278
|
-
const
|
|
342
|
+
return run(async (ctx) => {
|
|
343
|
+
const [entityType, bundle] = target.split("/");
|
|
344
|
+
const rctx = { command: "update", target };
|
|
345
|
+
if (entityType !== undefined)
|
|
346
|
+
rctx.entityType = entityType;
|
|
347
|
+
if (bundle !== undefined)
|
|
348
|
+
rctx.bundle = bundle;
|
|
349
|
+
const deps = { client: ctx.client, emit: (v) => output.emit(v, rctx) };
|
|
279
350
|
if (!args.noValidate) {
|
|
280
351
|
deps.validate = async (payload, t) => {
|
|
281
352
|
const schema = await loadOrFetchSchema(ctx, t, "update");
|
|
@@ -283,7 +354,7 @@ export function buildProgram(opts = {}) {
|
|
|
283
354
|
};
|
|
284
355
|
}
|
|
285
356
|
await runUpdate(args, deps);
|
|
286
|
-
});
|
|
357
|
+
}, assertRenderable);
|
|
287
358
|
});
|
|
288
359
|
program
|
|
289
360
|
.command("delete <target>")
|
|
@@ -294,7 +365,7 @@ export function buildProgram(opts = {}) {
|
|
|
294
365
|
const args = { target };
|
|
295
366
|
if (o.dryRun !== undefined)
|
|
296
367
|
args.dryRun = o.dryRun;
|
|
297
|
-
run((ctx) => runDelete(args, { client: ctx.client, emit: output.emit }));
|
|
368
|
+
return run((ctx) => runDelete(args, { client: ctx.client, emit: output.emit }), () => assertJsonOnly("delete"));
|
|
298
369
|
});
|
|
299
370
|
program
|
|
300
371
|
.command("upload-file")
|
|
@@ -307,7 +378,7 @@ export function buildProgram(opts = {}) {
|
|
|
307
378
|
const args = { target: o.target, file: o.file };
|
|
308
379
|
if (o.dryRun !== undefined)
|
|
309
380
|
args.dryRun = o.dryRun;
|
|
310
|
-
run((ctx) => runUploadFile(args, { client: ctx.client, emit: output.emit }));
|
|
381
|
+
return run((ctx) => runUploadFile(args, { client: ctx.client, emit: output.emit }), () => assertJsonOnly("upload-file"));
|
|
311
382
|
});
|
|
312
383
|
program
|
|
313
384
|
.command("schema [target]")
|
|
@@ -319,7 +390,7 @@ export function buildProgram(opts = {}) {
|
|
|
319
390
|
const schemaArgs = target !== undefined
|
|
320
391
|
? { target, operation, refresh: Boolean(o.refresh) }
|
|
321
392
|
: { operation, refresh: Boolean(o.refresh) };
|
|
322
|
-
run((ctx) => runSchema(schemaArgs, {
|
|
393
|
+
return run((ctx) => runSchema(schemaArgs, {
|
|
323
394
|
http: ctx.http,
|
|
324
395
|
auth: ctx.auth,
|
|
325
396
|
baseUrl: ctx.baseUrl,
|
|
@@ -328,7 +399,7 @@ export function buildProgram(opts = {}) {
|
|
|
328
399
|
emit: output.emit,
|
|
329
400
|
warn: (m) => stderr(`${m}\n`),
|
|
330
401
|
plugins: ctx.plugins,
|
|
331
|
-
}));
|
|
402
|
+
}), () => assertJsonOnly("schema"));
|
|
332
403
|
});
|
|
333
404
|
const auth = program.command("auth").description("Manage authentication");
|
|
334
405
|
auth
|
package/dist/src/plugin-api.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { basicAuthPlugin, basicAuthProvider } from "./core/auth/basic.js";
|
|
2
|
+
export { indexIncluded, isInteractive } from "./core/cli/render.js";
|
|
2
3
|
export { loadConfig } from "./core/config.js";
|
|
3
4
|
export { createHttpClient } from "./core/http.js";
|
|
4
5
|
export { createJsonApiClient } from "./core/jsonapi/client.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dropsh",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.3",
|
|
4
4
|
"description": "Entity-agnostic helper CLI for Drupal 11 JSON:API, used by the dropsh Claude skill for editorial publishing workflows.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -63,6 +63,7 @@
|
|
|
63
63
|
"lint:fix": "biome check --write src/ packages/ plugins/ tests/integrations/helpers/ tests/integrations/schema/ tests/integrations/*.ts",
|
|
64
64
|
"drupal:up": "bash tests/integrations/bin/drupal-up.sh",
|
|
65
65
|
"drupal:down": "bash tests/integrations/bin/drupal-down.sh",
|
|
66
|
+
"init-worktree": "node scripts/init-worktree.mjs",
|
|
66
67
|
"release": "node scripts/release.mjs"
|
|
67
68
|
}
|
|
68
69
|
}
|