dropsh 0.0.1 → 0.0.2
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 +302 -19
- package/bin/dropsh-src +0 -0
- package/dist/src/commands/auth.js +88 -0
- package/dist/src/commands/schema.js +48 -3
- package/dist/src/core/auth/basic.js +32 -10
- package/dist/src/core/auth/registry.js +9 -0
- package/dist/src/core/auth/session-store.js +38 -0
- package/dist/src/core/cli/prompt.js +53 -0
- package/dist/src/index.js +78 -9
- package/dist/src/plugin-api.js +1 -1
- package/package.json +21 -18
package/README.md
CHANGED
|
@@ -6,24 +6,74 @@ This is the foundation CLI. Discovery, schema generation, and the skill layer ar
|
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
9
|
+
This is a [pnpm](https://pnpm.io) workspace (root CLI + `plugins/*` + `packages/*`).
|
|
10
|
+
The pinned pnpm version is declared in `package.json` (`packageManager`); enable it with
|
|
11
|
+
`corepack enable`.
|
|
12
|
+
|
|
9
13
|
```bash
|
|
10
|
-
|
|
11
|
-
|
|
14
|
+
pnpm install
|
|
15
|
+
pnpm run build
|
|
12
16
|
```
|
|
13
17
|
|
|
14
18
|
## Configure
|
|
15
19
|
|
|
16
|
-
Copy
|
|
20
|
+
Copy `dropsh.config.example.js` to `dropsh.config.js` in your project. The config
|
|
21
|
+
carries only **non-secret connection parameters** — secrets (passwords, client
|
|
22
|
+
secrets, tokens) are never stored in the config. They are prompted at login and
|
|
23
|
+
persisted separately (see [Authentication](#authentication)).
|
|
24
|
+
|
|
25
|
+
```js
|
|
26
|
+
import { basicAuthPlugin } from "dropsh";
|
|
27
|
+
import { oauth2Plugin } from "@dropsh/plugin-oauth2";
|
|
28
|
+
|
|
29
|
+
export default {
|
|
30
|
+
site: { base_url: "https://my-drupal.example.com", jsonapi_prefix: "/jsonapi" },
|
|
31
|
+
defaults: { dry_run: false, timeout_ms: 30000 },
|
|
32
|
+
plugins: [
|
|
33
|
+
basicAuthPlugin(),
|
|
34
|
+
oauth2Plugin({ type: "oauth2_authcode", client_id: "my-client", token_url: "https://my-drupal.example.com/oauth/token" }),
|
|
35
|
+
],
|
|
36
|
+
};
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Authentication
|
|
40
|
+
|
|
41
|
+
Authentication is provider-based. Each entry in `plugins` that contributes an auth
|
|
42
|
+
provider (`basicAuthPlugin()`, `oauth2Plugin(...)`) becomes a login option. You log
|
|
43
|
+
in once per host; dropsh stores a single active session and uses it for every
|
|
44
|
+
subsequent command.
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
dropsh auth login # interactive picker over the configured providers
|
|
48
|
+
dropsh auth login --provider basic # skip the picker and use a provider by id
|
|
49
|
+
dropsh auth logout # clear the active session for the host
|
|
50
|
+
dropsh auth status # show the active session
|
|
51
|
+
dropsh auth status --json # machine-readable status
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`dropsh auth login` prompts for any secrets the chosen provider needs (basic auth
|
|
55
|
+
prompts username + password; OAuth2 password / client-credentials prompt the client
|
|
56
|
+
secret and, for the password grant, the user password; `oauth2_authcode` opens the
|
|
57
|
+
browser for a PKCE flow). On a non-interactive (non-TTY) shell you must pass
|
|
58
|
+
`--provider <id>`.
|
|
59
|
+
|
|
60
|
+
### Where secrets live
|
|
61
|
+
|
|
62
|
+
Secrets and tokens are **never** written to `dropsh.config.js`. The active session
|
|
63
|
+
is stored per host at `~/.config/dropsh/<host>.json` with file mode `0600`:
|
|
17
64
|
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
base_url: https://my-drupal.example.com
|
|
21
|
-
auth:
|
|
22
|
-
type: basic
|
|
23
|
-
username: ${DRUPAL_USER}
|
|
24
|
-
password: ${DRUPAL_PASSWORD}
|
|
65
|
+
```json
|
|
66
|
+
{ "activeProvider": "oauth2_authcode", "session": { "access_token": "…", "expires_at": 1717459200000 } }
|
|
25
67
|
```
|
|
26
68
|
|
|
69
|
+
Only non-secret connection parameters stay in the config:
|
|
70
|
+
|
|
71
|
+
- **basic** — `basicAuthPlugin()` (no fields; an optional `username` may be
|
|
72
|
+
pre-seeded, but the password is always prompted)
|
|
73
|
+
- **oauth2** — `client_id`, `token_url`, optional `scope`; `redirect_port` for
|
|
74
|
+
`oauth2_authcode`; `username` for `oauth2_password`. The `client_secret` and the
|
|
75
|
+
user `password` are prompted at login, never stored in config.
|
|
76
|
+
|
|
27
77
|
## Commands
|
|
28
78
|
|
|
29
79
|
All commands write JSON to stdout, structured errors to stderr, and use exit codes 0-5.
|
|
@@ -50,19 +100,240 @@ With a target (e.g. `node/article`), prints a JSON Schema document that validate
|
|
|
50
100
|
|
|
51
101
|
If the site has `drupal/schemata` + `drupal/schemata_json_schema` installed, the schema is authoritative (required fields, constraints). Otherwise, a shallow schema is returned from sample records with a warning on stderr (field names only, no required fields, no constraints). The output carries `x-dropsh-source: "schemata" | "heuristic" | "heuristic-empty"` so consumers can tell how strict the schema is.
|
|
52
102
|
|
|
53
|
-
Schemas are cached under `.dropsh/cache
|
|
103
|
+
Schemas are cached under `.dropsh/cache/`. Override the config path via `DROPSH_CONFIG` or `--config`.
|
|
104
|
+
|
|
105
|
+
### Authoritative schemas — `@dropsh/plugin-schemata`
|
|
106
|
+
|
|
107
|
+
Without a schema plugin, the schema is derived from sample records (field names
|
|
108
|
+
only). [`@dropsh/plugin-schemata`](plugins/schemata/README.md) returns the
|
|
109
|
+
authoritative schema (required fields + constraints) from Drupal's `schemata`
|
|
110
|
+
module, which makes client-side validation in `create` / `update` meaningful.
|
|
111
|
+
Requires the Drupal modules `schemata` + `jsonapi_schema`.
|
|
112
|
+
|
|
113
|
+
```js
|
|
114
|
+
import { schemataPlugin } from "@dropsh/plugin-schemata";
|
|
115
|
+
|
|
116
|
+
export default {
|
|
117
|
+
site: { base_url: "https://my-drupal.example.com", jsonapi_prefix: "/jsonapi" },
|
|
118
|
+
plugins: [basicAuthPlugin(), schemataPlugin()],
|
|
119
|
+
};
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
The output's `x-dropsh-source` is `"schemata"` when authoritative, or
|
|
123
|
+
`"heuristic"` / `"heuristic-empty"` for the sample-derived fallback.
|
|
124
|
+
|
|
125
|
+
### Canvas schema plugin
|
|
126
|
+
|
|
127
|
+
Canvas support lives in `@dropsh/plugin-canvas` and requires these Drupal modules:
|
|
128
|
+
|
|
129
|
+
- `canvas`
|
|
130
|
+
- `jsonapi_sdc`
|
|
131
|
+
|
|
132
|
+
Register the plugin in `dropsh.config.js`:
|
|
133
|
+
|
|
134
|
+
```js
|
|
135
|
+
import { basicAuthPlugin } from "dropsh";
|
|
136
|
+
import { canvasPlugin } from "@dropsh/plugin-canvas";
|
|
137
|
+
|
|
138
|
+
export default {
|
|
139
|
+
site: { base_url: "https://my-drupal.example.com", jsonapi_prefix: "/jsonapi" },
|
|
140
|
+
plugins: [
|
|
141
|
+
basicAuthPlugin(),
|
|
142
|
+
canvasPlugin(),
|
|
143
|
+
],
|
|
144
|
+
};
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Then ask for the Canvas schema:
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
dropsh schema canvas_page/canvas_page --for=create
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
The returned JSON Schema includes `x-dropsh-builder: "canvas"`, `x-dropsh-components`, and a typed `attributes.components` array. If `jsonapi_sdc` is missing or inaccessible, the Canvas schema fails instead of returning incomplete component information.
|
|
54
154
|
|
|
55
155
|
### Client-side validation in `create` / `update`
|
|
56
156
|
|
|
57
157
|
`dropsh create` and `dropsh update` run the payload through the bundle's schema before sending it. A validation failure exits with code 4 (`E_VALIDATION`) and emits the Ajv errors on stderr without issuing an HTTP request. Pass `--no-validate` to skip the check.
|
|
58
158
|
|
|
159
|
+
## Writing plugins
|
|
160
|
+
|
|
161
|
+
A plugin is a function that returns a `DropSHPlugin`. Plugins can do three
|
|
162
|
+
independent things — extend schemas, provide an auth provider, and register CLI
|
|
163
|
+
commands — and a single plugin may combine any of them. Everything you need is
|
|
164
|
+
re-exported from `dropsh/plugin`:
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
import type {
|
|
168
|
+
DropSHPlugin, PluginContext, SchemaOperation, // schema extension
|
|
169
|
+
AuthProvider, AuthContext, AuthSession, AdapterRuntime, AuthAdapter, // auth
|
|
170
|
+
} from "dropsh/plugin";
|
|
171
|
+
import { AuthError, ConfigError, HttpError, createHttpClient } from "dropsh/plugin";
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
The `DropSHPlugin` shape:
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
interface DropSHPlugin {
|
|
178
|
+
readonly id: string; // unique id, also the auth-provider key
|
|
179
|
+
readonly requiredModules: string[]; // Drupal modules the plugin needs
|
|
180
|
+
authProvider?: AuthProvider; // optional: contributes a login option
|
|
181
|
+
extendSchema(entityType, bundle, baseSchema, ctx): Promise<unknown>;
|
|
182
|
+
extendOperationSchema?(entityType, bundle, operation, schema, ctx): Promise<unknown>;
|
|
183
|
+
registerCommands?(program): void; // optional: add Commander subcommands
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
A plugin is wired up by adding it to `plugins` in `dropsh.config.js`. Package it
|
|
188
|
+
as its own npm module with `dropsh` as a **peer dependency** (so it shares the
|
|
189
|
+
host's core), or keep it local and import it directly.
|
|
190
|
+
|
|
191
|
+
### A schema-extension plugin
|
|
192
|
+
|
|
193
|
+
`extendSchema` is called while building the JSON Schema for a target. It receives
|
|
194
|
+
the schema produced so far and returns a (possibly) modified schema. Return
|
|
195
|
+
`baseSchema` unchanged when the plugin does not apply. `extendOperationSchema`
|
|
196
|
+
runs later, per operation (`create` / `update`), when you need to vary the schema
|
|
197
|
+
by operation. `ctx: PluginContext` gives you `{ http, auth, baseUrl }` to call the
|
|
198
|
+
site if you need live data.
|
|
199
|
+
|
|
200
|
+
```ts
|
|
201
|
+
import type { DropSHPlugin, PluginContext, SchemaOperation } from "dropsh/plugin";
|
|
202
|
+
|
|
203
|
+
// Marks node/article's `field_legacy_id` read-only and adds a provenance tag.
|
|
204
|
+
export function articleRulesPlugin(): DropSHPlugin {
|
|
205
|
+
return {
|
|
206
|
+
id: "article-rules",
|
|
207
|
+
requiredModules: [],
|
|
208
|
+
|
|
209
|
+
async extendSchema(entityType: string, bundle: string, baseSchema: unknown, _ctx: PluginContext) {
|
|
210
|
+
if (entityType !== "node" || bundle !== "article") return baseSchema;
|
|
211
|
+
const schema = baseSchema as {
|
|
212
|
+
properties?: { attributes?: { properties?: Record<string, unknown> } };
|
|
213
|
+
};
|
|
214
|
+
const attrs = schema.properties?.attributes?.properties;
|
|
215
|
+
if (attrs?.field_legacy_id && typeof attrs.field_legacy_id === "object") {
|
|
216
|
+
(attrs.field_legacy_id as Record<string, unknown>).readOnly = true;
|
|
217
|
+
}
|
|
218
|
+
return { ...schema, "x-extended-by": "article-rules" };
|
|
219
|
+
},
|
|
220
|
+
|
|
221
|
+
// Optional: on create, require a field that is optional on update.
|
|
222
|
+
async extendOperationSchema(
|
|
223
|
+
entityType: string,
|
|
224
|
+
bundle: string,
|
|
225
|
+
operation: SchemaOperation,
|
|
226
|
+
schema: unknown,
|
|
227
|
+
_ctx: PluginContext,
|
|
228
|
+
) {
|
|
229
|
+
if (entityType !== "node" || bundle !== "article" || operation !== "create") return schema;
|
|
230
|
+
const s = schema as { required?: string[] };
|
|
231
|
+
const required = new Set(s.required ?? []);
|
|
232
|
+
required.add("field_legacy_id");
|
|
233
|
+
return { ...s, required: [...required] };
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
Register it:
|
|
240
|
+
|
|
241
|
+
```js
|
|
242
|
+
import { articleRulesPlugin } from "./plugins/article-rules.js";
|
|
243
|
+
export default {
|
|
244
|
+
site: { base_url: "https://my-drupal.example.com", jsonapi_prefix: "/jsonapi" },
|
|
245
|
+
plugins: [basicAuthPlugin(), articleRulesPlugin()],
|
|
246
|
+
};
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
The bundled `@dropsh/plugin-schemata` (authoritative schemas from the Drupal
|
|
250
|
+
`schemata` module) and `@dropsh/plugin-canvas` (component schemas) are real
|
|
251
|
+
examples of schema plugins — see `plugins/schemata/src/index.ts` and
|
|
252
|
+
`plugins/canvas/src/index.ts`.
|
|
253
|
+
|
|
254
|
+
### An auth-provider plugin
|
|
255
|
+
|
|
256
|
+
An auth provider is a single `AuthProvider` object exposed via the plugin's
|
|
257
|
+
`authProvider` field. It owns one protocol end-to-end: `login` acquires a session
|
|
258
|
+
(prompting / browser as needed), `createAdapter` turns a stored session into the
|
|
259
|
+
`apply(req)` runtime hook, and `logout` / `status` round it out. The core handles
|
|
260
|
+
the picker, the single-session state file, and dispatch — the provider never
|
|
261
|
+
touches stdin or disk directly; it uses the injected `AuthContext` / `AdapterRuntime`.
|
|
262
|
+
|
|
263
|
+
```ts
|
|
264
|
+
import type {
|
|
265
|
+
AdapterRuntime, AuthAdapter, AuthContext, AuthProvider, AuthSession, DropSHPlugin,
|
|
266
|
+
} from "dropsh/plugin";
|
|
267
|
+
import { ConfigError } from "dropsh/plugin";
|
|
268
|
+
|
|
269
|
+
// Static API-key auth: prompts for a token at login, sends it as a header.
|
|
270
|
+
export function apiKeyProvider(): AuthProvider {
|
|
271
|
+
return {
|
|
272
|
+
id: "apikey",
|
|
273
|
+
displayName: "API key (X-API-Key header)",
|
|
274
|
+
capabilities: { login: true, logout: true, status: true },
|
|
275
|
+
|
|
276
|
+
async login(ctx: AuthContext): Promise<AuthSession> {
|
|
277
|
+
const key = await ctx.prompt({ label: "API key", secret: true });
|
|
278
|
+
if (!key) throw new ConfigError("apikey: a non-empty key is required");
|
|
279
|
+
return { api_key: key };
|
|
280
|
+
},
|
|
281
|
+
|
|
282
|
+
async logout(_ctx: AuthContext) {
|
|
283
|
+
// No server-side revoke; the core clears the local session file.
|
|
284
|
+
},
|
|
285
|
+
|
|
286
|
+
async status(session: AuthSession | null) {
|
|
287
|
+
return { loggedIn: session !== null, provider: "apikey" };
|
|
288
|
+
},
|
|
289
|
+
|
|
290
|
+
createAdapter(session: AuthSession, _rt: AdapterRuntime): AuthAdapter {
|
|
291
|
+
const key = session.api_key;
|
|
292
|
+
if (typeof key !== "string") throw new ConfigError("apikey: corrupt session");
|
|
293
|
+
return {
|
|
294
|
+
async apply(req) {
|
|
295
|
+
return { ...req, headers: { ...(req.headers ?? {}), "X-API-Key": key } };
|
|
296
|
+
},
|
|
297
|
+
};
|
|
298
|
+
},
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export function apiKeyPlugin(): DropSHPlugin {
|
|
303
|
+
return {
|
|
304
|
+
id: "apikey",
|
|
305
|
+
requiredModules: [],
|
|
306
|
+
authProvider: apiKeyProvider(),
|
|
307
|
+
async extendSchema(_entityType, _bundle, schema) {
|
|
308
|
+
return schema; // not a schema plugin
|
|
309
|
+
},
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
Once registered in `plugins`, it shows up automatically in `dropsh auth login`:
|
|
315
|
+
|
|
316
|
+
```bash
|
|
317
|
+
dropsh auth login --provider apikey
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
`AuthContext` (passed to `login`/`logout`) provides `baseUrl`, an `http` client,
|
|
321
|
+
`prompt({ label, secret })`, `openBrowser(url)`, `stdout(s)`, and `now()`.
|
|
322
|
+
`AdapterRuntime` (passed to `createAdapter`) provides `http`, `now()`, and
|
|
323
|
+
`save(session)` — call `save` to persist a refreshed session back to the active
|
|
324
|
+
slot (the OAuth2 `oauth2_authcode` provider uses this to store a refreshed token).
|
|
325
|
+
Throw `AuthError` for expired/invalid sessions so the CLI exits with code 3 and
|
|
326
|
+
tells the user to run `dropsh auth login`. The bundled `@dropsh/plugin-oauth2`
|
|
327
|
+
(`plugins/oauth2/src/provider.ts`) and the core basic provider
|
|
328
|
+
(`src/core/auth/basic.ts`) are full reference implementations.
|
|
329
|
+
|
|
59
330
|
## Development
|
|
60
331
|
|
|
61
332
|
```bash
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
333
|
+
pnpm test # Vitest unit suite
|
|
334
|
+
pnpm run test:watch
|
|
335
|
+
pnpm run typecheck
|
|
336
|
+
pnpm run build
|
|
66
337
|
```
|
|
67
338
|
|
|
68
339
|
### Integration tests
|
|
@@ -70,9 +341,21 @@ npm run build
|
|
|
70
341
|
Local-only, requires DDEV. See `tests/integrations/README.md`.
|
|
71
342
|
|
|
72
343
|
```bash
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
344
|
+
pnpm run drupal:up
|
|
345
|
+
pnpm run test:integration
|
|
346
|
+
pnpm run drupal:down
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
## Publishing
|
|
350
|
+
|
|
351
|
+
The CLI and all workspace packages are published together with a single command:
|
|
352
|
+
|
|
353
|
+
```bash
|
|
354
|
+
pnpm run release
|
|
76
355
|
```
|
|
77
356
|
|
|
78
|
-
|
|
357
|
+
This publishes `dropsh` (workspace root) plus `@dropsh/sdc-client`,
|
|
358
|
+
`@dropsh/plugin-schemata`, `@dropsh/plugin-oauth2` and `@dropsh/plugin-canvas`.
|
|
359
|
+
`prepublishOnly` runs typecheck + build per package; scoped packages publish with
|
|
360
|
+
public access via their `publishConfig`, and pnpm rewrites `workspace:*` deps to the
|
|
361
|
+
released version. Log in first with `npm login`.
|
package/bin/dropsh-src
CHANGED
|
File without changes
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { loginCapableProviders, providerById } from "../core/auth/registry.js";
|
|
2
|
+
import { clearSession, readSession, writeSession } from "../core/auth/session-store.js";
|
|
3
|
+
import { AuthError, ConfigError } from "../errors.js";
|
|
4
|
+
function authContext(deps) {
|
|
5
|
+
const ctx = {
|
|
6
|
+
baseUrl: deps.baseUrl,
|
|
7
|
+
http: deps.http,
|
|
8
|
+
prompt: deps.prompt,
|
|
9
|
+
openBrowser: deps.openBrowser,
|
|
10
|
+
stdout: deps.stdout,
|
|
11
|
+
now: deps.now,
|
|
12
|
+
};
|
|
13
|
+
if (deps.stateDir !== undefined)
|
|
14
|
+
ctx.stateDir = deps.stateDir;
|
|
15
|
+
return ctx;
|
|
16
|
+
}
|
|
17
|
+
async function pickProvider(deps, requestedId) {
|
|
18
|
+
const capable = loginCapableProviders(deps.providers);
|
|
19
|
+
if (capable.length === 0)
|
|
20
|
+
throw new ConfigError("no login-capable auth provider configured");
|
|
21
|
+
if (requestedId) {
|
|
22
|
+
const found = providerById(capable, requestedId);
|
|
23
|
+
if (!found)
|
|
24
|
+
throw new ConfigError(`unknown provider '${requestedId}'. Available: ${capable.map((p) => p.id).join(", ")}`);
|
|
25
|
+
return found;
|
|
26
|
+
}
|
|
27
|
+
if (!deps.isTTY)
|
|
28
|
+
throw new ConfigError("non-interactive: pass --provider <id>");
|
|
29
|
+
deps.stdout("Select an auth provider:\n");
|
|
30
|
+
capable.forEach((p, i) => {
|
|
31
|
+
deps.stdout(` ${i + 1}) ${p.displayName} [${p.id}]\n`);
|
|
32
|
+
});
|
|
33
|
+
const answer = await deps.prompt({ label: "Number" });
|
|
34
|
+
const idx = Number.parseInt(answer, 10) - 1;
|
|
35
|
+
const chosen = capable[idx];
|
|
36
|
+
if (!chosen)
|
|
37
|
+
throw new ConfigError(`invalid selection '${answer}'`);
|
|
38
|
+
return chosen;
|
|
39
|
+
}
|
|
40
|
+
export async function runAuthLogin(args, deps) {
|
|
41
|
+
const provider = await pickProvider(deps, args.provider);
|
|
42
|
+
const session = await provider.login(authContext(deps));
|
|
43
|
+
await writeSession(deps.baseUrl, provider.id, session, deps.stateDir);
|
|
44
|
+
deps.stdout(`logged in via ${provider.displayName}\n`);
|
|
45
|
+
}
|
|
46
|
+
export async function runAuthLogout(_args, deps) {
|
|
47
|
+
const rec = await readSession(deps.baseUrl, deps.stateDir);
|
|
48
|
+
if (!rec) {
|
|
49
|
+
deps.stdout("not logged in\n");
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const provider = providerById(deps.providers, rec.activeProvider);
|
|
53
|
+
try {
|
|
54
|
+
if (provider)
|
|
55
|
+
await provider.logout(authContext(deps));
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
// Best-effort revoke: never block local logout on a failing revoke call.
|
|
59
|
+
deps.stderr(`warning: provider logout failed: ${String(err)}\n`);
|
|
60
|
+
}
|
|
61
|
+
finally {
|
|
62
|
+
await clearSession(deps.baseUrl, deps.stateDir);
|
|
63
|
+
}
|
|
64
|
+
deps.stdout("logged out\n");
|
|
65
|
+
}
|
|
66
|
+
export async function runAuthStatus(args, deps) {
|
|
67
|
+
const rec = await readSession(deps.baseUrl, deps.stateDir);
|
|
68
|
+
if (!rec) {
|
|
69
|
+
if (args.json)
|
|
70
|
+
deps.stdout(`${JSON.stringify({ loggedIn: false })}\n`);
|
|
71
|
+
else
|
|
72
|
+
deps.stdout("not logged in\n");
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const provider = providerById(deps.providers, rec.activeProvider);
|
|
76
|
+
if (!provider)
|
|
77
|
+
throw new AuthError(`active provider '${rec.activeProvider}' is not configured`);
|
|
78
|
+
const info = await provider.status(rec.session);
|
|
79
|
+
const host = new URL(deps.baseUrl).hostname;
|
|
80
|
+
if (args.json) {
|
|
81
|
+
deps.stdout(`${JSON.stringify({ ...info, host })}\n`);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
deps.stdout(`provider: ${provider.displayName} [${provider.id}]\n`);
|
|
85
|
+
deps.stdout(`host: ${host}\n`);
|
|
86
|
+
if (info.expiresAt !== undefined)
|
|
87
|
+
deps.stdout(`token: ${info.state} (until ${new Date(info.expiresAt).toISOString()})\n`);
|
|
88
|
+
}
|
|
@@ -4,6 +4,40 @@ import { fetchCatalog } from "../core/schema/catalog.js";
|
|
|
4
4
|
import { fetchJsonSchema } from "../core/schema/jsonschema-source.js";
|
|
5
5
|
import { toOperationVariant } from "../core/schema/to-jsonschema.js";
|
|
6
6
|
import { ValidationError } from "../errors.js";
|
|
7
|
+
export const SCHEMA_PIPELINE_VERSION = 2;
|
|
8
|
+
function isRecord(value) {
|
|
9
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
10
|
+
}
|
|
11
|
+
export function operationHookPluginIds(plugins) {
|
|
12
|
+
return plugins.filter((plugin) => plugin.extendOperationSchema).map((plugin) => plugin.id);
|
|
13
|
+
}
|
|
14
|
+
export function schemaCacheMetadataMatches(schema, operationHookPlugins) {
|
|
15
|
+
if (!isRecord(schema))
|
|
16
|
+
return false;
|
|
17
|
+
if (schema["x-dropsh-schema-pipeline-version"] !== SCHEMA_PIPELINE_VERSION)
|
|
18
|
+
return false;
|
|
19
|
+
const cachedPlugins = schema["x-dropsh-operation-hook-plugins"];
|
|
20
|
+
if (!Array.isArray(cachedPlugins))
|
|
21
|
+
return false;
|
|
22
|
+
if (cachedPlugins.length !== operationHookPlugins.length)
|
|
23
|
+
return false;
|
|
24
|
+
return cachedPlugins.every((plugin, index) => plugin === operationHookPlugins[index]);
|
|
25
|
+
}
|
|
26
|
+
export async function applyOperationSchemaPlugins(schema, args, deps) {
|
|
27
|
+
let current = schema;
|
|
28
|
+
const extensions = [];
|
|
29
|
+
const ctx = { http: deps.http, auth: deps.auth, baseUrl: deps.baseUrl };
|
|
30
|
+
for (const plugin of deps.plugins) {
|
|
31
|
+
if (!plugin.extendOperationSchema)
|
|
32
|
+
continue;
|
|
33
|
+
const extended = await plugin.extendOperationSchema(args.entity, args.bundle, args.operation, current, ctx);
|
|
34
|
+
if (extended !== current) {
|
|
35
|
+
current = extended;
|
|
36
|
+
extensions.push(plugin.id);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return { schema: current, extensions };
|
|
40
|
+
}
|
|
7
41
|
const TARGET_RE = /^[a-z0-9_]+\/[a-z0-9_]+$/;
|
|
8
42
|
export async function runSchema(args, deps) {
|
|
9
43
|
const store = createFileStore({ rootDir: join(deps.cwd, ".dropsh/cache"), warn: deps.warn });
|
|
@@ -32,9 +66,11 @@ export async function runSchema(args, deps) {
|
|
|
32
66
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
33
67
|
const [entity, bundle] = args.target.split("/", 2);
|
|
34
68
|
const cacheKey = `schema/${entity}--${bundle}.${args.operation}.json`;
|
|
69
|
+
const plugins = deps.plugins ?? [];
|
|
70
|
+
const hookPluginIds = operationHookPluginIds(plugins);
|
|
35
71
|
if (!args.refresh) {
|
|
36
72
|
const hit = await store.read(cacheKey);
|
|
37
|
-
if (hit !== undefined) {
|
|
73
|
+
if (hit !== undefined && schemaCacheMetadataMatches(hit, hookPluginIds)) {
|
|
38
74
|
deps.emit(hit);
|
|
39
75
|
return;
|
|
40
76
|
}
|
|
@@ -47,14 +83,23 @@ export async function runSchema(args, deps) {
|
|
|
47
83
|
entity,
|
|
48
84
|
bundle,
|
|
49
85
|
warn: deps.warn,
|
|
50
|
-
plugins
|
|
86
|
+
plugins,
|
|
51
87
|
});
|
|
52
88
|
const transformed = toOperationVariant(raw, args.operation);
|
|
89
|
+
const operationExtended = await applyOperationSchemaPlugins(transformed, { entity, bundle, operation: args.operation }, {
|
|
90
|
+
http: deps.http,
|
|
91
|
+
auth: deps.auth,
|
|
92
|
+
baseUrl: deps.baseUrl,
|
|
93
|
+
plugins,
|
|
94
|
+
});
|
|
53
95
|
const tagged = {
|
|
54
|
-
...
|
|
96
|
+
...operationExtended.schema,
|
|
55
97
|
"x-dropsh-source": source,
|
|
56
98
|
"x-dropsh-target": { entity_type: entity, bundle },
|
|
57
99
|
"x-dropsh-operation": args.operation,
|
|
100
|
+
"x-dropsh-schema-extensions": operationExtended.extensions,
|
|
101
|
+
"x-dropsh-schema-pipeline-version": SCHEMA_PIPELINE_VERSION,
|
|
102
|
+
"x-dropsh-operation-hook-plugins": hookPluginIds,
|
|
58
103
|
};
|
|
59
104
|
await store.write(cacheKey, tagged);
|
|
60
105
|
deps.emit(tagged);
|
|
@@ -1,22 +1,44 @@
|
|
|
1
1
|
import { ConfigError } from "../../errors.js";
|
|
2
|
-
|
|
3
|
-
if (typeof cfg.username !== "string" || cfg.username.length === 0)
|
|
4
|
-
throw new ConfigError("basicAuthPlugin: username required");
|
|
5
|
-
if (typeof cfg.password !== "string" || cfg.password.length === 0)
|
|
6
|
-
throw new ConfigError("basicAuthPlugin: password required");
|
|
7
|
-
const token = Buffer.from(`${cfg.username}:${cfg.password}`).toString("base64");
|
|
2
|
+
function adapterFromB64(b64) {
|
|
8
3
|
return {
|
|
9
4
|
async apply(req) {
|
|
10
|
-
return { ...req, headers: { ...(req.headers ?? {}), Authorization: `Basic ${
|
|
5
|
+
return { ...req, headers: { ...(req.headers ?? {}), Authorization: `Basic ${b64}` } };
|
|
11
6
|
},
|
|
12
7
|
};
|
|
13
8
|
}
|
|
14
|
-
export function
|
|
15
|
-
|
|
9
|
+
export function basicAuthProvider(config = {}) {
|
|
10
|
+
return {
|
|
11
|
+
id: "basic",
|
|
12
|
+
displayName: "Basic auth (username / password)",
|
|
13
|
+
capabilities: { login: true, logout: true, status: true },
|
|
14
|
+
async login(ctx) {
|
|
15
|
+
const username = config.username ?? (await ctx.prompt({ label: "Username" }));
|
|
16
|
+
if (!username)
|
|
17
|
+
throw new ConfigError("basic auth: username required");
|
|
18
|
+
const password = await ctx.prompt({ label: "Password", secret: true });
|
|
19
|
+
if (!password)
|
|
20
|
+
throw new ConfigError("basic auth: password required");
|
|
21
|
+
return { basic_b64: Buffer.from(`${username}:${password}`).toString("base64") };
|
|
22
|
+
},
|
|
23
|
+
async logout() {
|
|
24
|
+
// No server-side revoke for basic auth; the core clears the session.
|
|
25
|
+
},
|
|
26
|
+
async status(session) {
|
|
27
|
+
return { loggedIn: session !== null, provider: "basic" };
|
|
28
|
+
},
|
|
29
|
+
createAdapter(session) {
|
|
30
|
+
const b64 = session.basic_b64;
|
|
31
|
+
if (typeof b64 !== "string")
|
|
32
|
+
throw new ConfigError("basic auth: corrupt session");
|
|
33
|
+
return adapterFromB64(b64);
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
export function basicAuthPlugin(config = {}) {
|
|
16
38
|
return {
|
|
17
39
|
id: "basic",
|
|
18
40
|
requiredModules: [],
|
|
19
|
-
|
|
41
|
+
authProvider: basicAuthProvider(config),
|
|
20
42
|
async extendSchema(_entityType, _bundle, schema, _ctx) {
|
|
21
43
|
return schema;
|
|
22
44
|
},
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export function collectProviders(plugins) {
|
|
2
|
+
return plugins.map((p) => p.authProvider).filter((p) => p !== undefined);
|
|
3
|
+
}
|
|
4
|
+
export function loginCapableProviders(providers) {
|
|
5
|
+
return providers.filter((p) => p.capabilities.login);
|
|
6
|
+
}
|
|
7
|
+
export function providerById(providers, id) {
|
|
8
|
+
return providers.find((p) => p.id === id);
|
|
9
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { chmod, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
export function defaultStateDir() {
|
|
5
|
+
return join(homedir(), ".config", "dropsh");
|
|
6
|
+
}
|
|
7
|
+
function hostnameFromUrl(baseUrl) {
|
|
8
|
+
return new URL(baseUrl).hostname;
|
|
9
|
+
}
|
|
10
|
+
function sessionPath(baseUrl, dir) {
|
|
11
|
+
return join(dir, `${hostnameFromUrl(baseUrl)}.json`);
|
|
12
|
+
}
|
|
13
|
+
export async function readSession(baseUrl, dir) {
|
|
14
|
+
const path = sessionPath(baseUrl, dir ?? defaultStateDir());
|
|
15
|
+
try {
|
|
16
|
+
const raw = await readFile(path, "utf8");
|
|
17
|
+
return JSON.parse(raw);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export async function writeSession(baseUrl, activeProvider, session, dir) {
|
|
24
|
+
const stateDir = dir ?? defaultStateDir();
|
|
25
|
+
await mkdir(stateDir, { recursive: true });
|
|
26
|
+
const record = { activeProvider, session };
|
|
27
|
+
const path = sessionPath(baseUrl, stateDir);
|
|
28
|
+
await writeFile(path, JSON.stringify(record, null, 2), {
|
|
29
|
+
encoding: "utf8",
|
|
30
|
+
mode: 0o600,
|
|
31
|
+
});
|
|
32
|
+
// writeFile's `mode` only applies when creating a new file; overwriting an
|
|
33
|
+
// existing file keeps its old (possibly looser) permissions, so tighten explicitly.
|
|
34
|
+
await chmod(path, 0o600);
|
|
35
|
+
}
|
|
36
|
+
export async function clearSession(baseUrl, dir) {
|
|
37
|
+
await rm(sessionPath(baseUrl, dir ?? defaultStateDir()), { force: true });
|
|
38
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { createInterface } from "node:readline";
|
|
2
|
+
/**
|
|
3
|
+
* Build a prompt function. Secret prompts use raw mode + manual key
|
|
4
|
+
* accumulation on a real TTY so the input is never echoed. On a non-TTY
|
|
5
|
+
* stream (tests/pipes) input is read as a line so the value is still captured.
|
|
6
|
+
*/
|
|
7
|
+
export function createPrompt(deps = {}) {
|
|
8
|
+
const input = deps.input ?? process.stdin;
|
|
9
|
+
const write = deps.write ?? ((s) => process.stderr.write(s));
|
|
10
|
+
const isTTY = deps.isTTY ?? Boolean(input.isTTY);
|
|
11
|
+
return ({ label, secret }) => new Promise((resolve, reject) => {
|
|
12
|
+
write(`${label}: `);
|
|
13
|
+
if (secret && isTTY) {
|
|
14
|
+
const stdin = input;
|
|
15
|
+
const prevRaw = stdin.isRaw === true;
|
|
16
|
+
let buf = "";
|
|
17
|
+
stdin.setRawMode?.(true);
|
|
18
|
+
stdin.resume();
|
|
19
|
+
const cleanup = () => {
|
|
20
|
+
stdin.setRawMode?.(prevRaw);
|
|
21
|
+
stdin.pause();
|
|
22
|
+
stdin.off("data", onData);
|
|
23
|
+
};
|
|
24
|
+
const onData = (d) => {
|
|
25
|
+
for (const ch of d.toString("utf8")) {
|
|
26
|
+
if (ch === "\n" || ch === "\r") {
|
|
27
|
+
cleanup();
|
|
28
|
+
write("\n");
|
|
29
|
+
resolve(buf);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (ch === "\u0003") {
|
|
33
|
+
cleanup();
|
|
34
|
+
reject(new Error("input cancelled"));
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (ch === "\u007f" || ch === "\b")
|
|
38
|
+
buf = buf.slice(0, -1);
|
|
39
|
+
else
|
|
40
|
+
buf += ch;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
stdin.on("data", onData);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
// Non-secret, or non-TTY (tests/pipes): read a single line.
|
|
47
|
+
const rl = createInterface({ input });
|
|
48
|
+
rl.once("line", (line) => {
|
|
49
|
+
rl.close();
|
|
50
|
+
resolve(line.replace(/\r?\n$/, ""));
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
}
|
package/dist/src/index.js
CHANGED
|
@@ -1,33 +1,51 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { parseArgs } from "node:util";
|
|
3
3
|
import { Command } from "commander";
|
|
4
|
+
import { runAuthLogin, runAuthLogout, runAuthStatus } from "./commands/auth.js";
|
|
4
5
|
import { runCreate } from "./commands/create.js";
|
|
5
6
|
import { runDelete } from "./commands/delete.js";
|
|
6
7
|
import { runRead } from "./commands/read.js";
|
|
7
|
-
import { runSchema } from "./commands/schema.js";
|
|
8
|
+
import { applyOperationSchemaPlugins, operationHookPluginIds, runSchema, SCHEMA_PIPELINE_VERSION, schemaCacheMetadataMatches, } from "./commands/schema.js";
|
|
8
9
|
import { runSearch } from "./commands/search.js";
|
|
9
10
|
import { runUpdate } from "./commands/update.js";
|
|
10
11
|
import { runUploadFile } from "./commands/upload-file.js";
|
|
12
|
+
import { collectProviders, providerById } from "./core/auth/registry.js";
|
|
13
|
+
import { readSession, writeSession } from "./core/auth/session-store.js";
|
|
11
14
|
import { createFileStore } from "./core/cache/file-store.js";
|
|
12
15
|
import { createOutput } from "./core/cli/output.js";
|
|
16
|
+
import { createPrompt } from "./core/cli/prompt.js";
|
|
13
17
|
import { loadConfig } from "./core/config.js";
|
|
14
18
|
import { createHttpClient } from "./core/http.js";
|
|
15
19
|
import { createJsonApiClient } from "./core/jsonapi/client.js";
|
|
16
20
|
import { fetchJsonSchema } from "./core/schema/jsonschema-source.js";
|
|
17
21
|
import { toOperationVariant } from "./core/schema/to-jsonschema.js";
|
|
18
22
|
import { validatePayload } from "./core/schema/validate.js";
|
|
19
|
-
import { ConfigError, exitCodeFor } from "./errors.js";
|
|
23
|
+
import { AuthError, ConfigError, exitCodeFor } from "./errors.js";
|
|
20
24
|
function resolveConfigPath(override) {
|
|
21
25
|
return override ?? process.env.DROPSH_CONFIG ?? "dropsh.config.js";
|
|
22
26
|
}
|
|
27
|
+
export async function resolveAuth(deps) {
|
|
28
|
+
const rec = await readSession(deps.baseUrl, deps.stateDir);
|
|
29
|
+
if (!rec)
|
|
30
|
+
throw new AuthError("Not authenticated. Run 'dropsh auth login'.");
|
|
31
|
+
const provider = providerById(collectProviders(deps.plugins), rec.activeProvider);
|
|
32
|
+
if (!provider)
|
|
33
|
+
throw new ConfigError(`active provider '${rec.activeProvider}' is not configured`);
|
|
34
|
+
return provider.createAdapter(rec.session, {
|
|
35
|
+
http: deps.http,
|
|
36
|
+
now: deps.now,
|
|
37
|
+
save: (session) => writeSession(deps.baseUrl, provider.id, session, deps.stateDir),
|
|
38
|
+
});
|
|
39
|
+
}
|
|
23
40
|
async function defaultContext(configPath) {
|
|
24
41
|
const cfg = await loadConfig(configPath);
|
|
25
42
|
const http = createHttpClient({ timeoutMs: cfg.defaults.timeout_ms });
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
43
|
+
const auth = await resolveAuth({
|
|
44
|
+
baseUrl: cfg.site.base_url,
|
|
45
|
+
plugins: cfg.plugins,
|
|
46
|
+
http,
|
|
47
|
+
now: Date.now,
|
|
48
|
+
});
|
|
31
49
|
const client = createJsonApiClient({
|
|
32
50
|
baseUrl: cfg.site.base_url,
|
|
33
51
|
prefix: cfg.site.jsonapi_prefix,
|
|
@@ -70,6 +88,32 @@ export function buildProgram(opts = {}) {
|
|
|
70
88
|
setExitCode(exitCodeFor(err));
|
|
71
89
|
}
|
|
72
90
|
}
|
|
91
|
+
async function run2(fn) {
|
|
92
|
+
try {
|
|
93
|
+
await fn();
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
output.fail(err);
|
|
97
|
+
setExitCode(exitCodeFor(err));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
async function authDeps() {
|
|
101
|
+
const cfg = await loadConfig(resolveConfigPath(program.opts().config));
|
|
102
|
+
return {
|
|
103
|
+
baseUrl: cfg.site.base_url,
|
|
104
|
+
providers: collectProviders(cfg.plugins),
|
|
105
|
+
stdout,
|
|
106
|
+
stderr,
|
|
107
|
+
prompt: createPrompt(),
|
|
108
|
+
openBrowser: async (url) => {
|
|
109
|
+
const { default: open } = await import("open");
|
|
110
|
+
await open(url);
|
|
111
|
+
},
|
|
112
|
+
http: createHttpClient({ timeoutMs: cfg.defaults.timeout_ms }),
|
|
113
|
+
now: Date.now,
|
|
114
|
+
isTTY: Boolean(process.stdin.isTTY),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
73
117
|
async function loadOrFetchSchema(ctx, target, op) {
|
|
74
118
|
const store = createFileStore({
|
|
75
119
|
rootDir: `${ctx.cwd}/.dropsh/cache`,
|
|
@@ -77,8 +121,9 @@ export function buildProgram(opts = {}) {
|
|
|
77
121
|
});
|
|
78
122
|
const [entity, bundle] = target.split("/", 2);
|
|
79
123
|
const key = `schema/${entity}--${bundle}.${op}.json`;
|
|
124
|
+
const hookPluginIds = operationHookPluginIds(ctx.plugins);
|
|
80
125
|
const hit = await store.read(key);
|
|
81
|
-
if (hit !== undefined)
|
|
126
|
+
if (hit !== undefined && schemaCacheMetadataMatches(hit, hookPluginIds))
|
|
82
127
|
return hit;
|
|
83
128
|
const { schema: raw, source } = await fetchJsonSchema({
|
|
84
129
|
http: ctx.http,
|
|
@@ -91,11 +136,20 @@ export function buildProgram(opts = {}) {
|
|
|
91
136
|
plugins: ctx.plugins,
|
|
92
137
|
});
|
|
93
138
|
const transformed = toOperationVariant(raw, op);
|
|
139
|
+
const operationExtended = await applyOperationSchemaPlugins(transformed, { entity, bundle, operation: op }, {
|
|
140
|
+
http: ctx.http,
|
|
141
|
+
auth: ctx.auth,
|
|
142
|
+
baseUrl: ctx.baseUrl,
|
|
143
|
+
plugins: ctx.plugins,
|
|
144
|
+
});
|
|
94
145
|
const tagged = {
|
|
95
|
-
...
|
|
146
|
+
...operationExtended.schema,
|
|
96
147
|
"x-dropsh-source": source,
|
|
97
148
|
"x-dropsh-target": { entity_type: entity, bundle },
|
|
98
149
|
"x-dropsh-operation": op,
|
|
150
|
+
"x-dropsh-schema-extensions": operationExtended.extensions,
|
|
151
|
+
"x-dropsh-schema-pipeline-version": SCHEMA_PIPELINE_VERSION,
|
|
152
|
+
"x-dropsh-operation-hook-plugins": hookPluginIds,
|
|
99
153
|
};
|
|
100
154
|
await store.write(key, tagged);
|
|
101
155
|
return tagged;
|
|
@@ -209,6 +263,21 @@ export function buildProgram(opts = {}) {
|
|
|
209
263
|
plugins: ctx.plugins,
|
|
210
264
|
}));
|
|
211
265
|
});
|
|
266
|
+
const auth = program.command("auth").description("Manage authentication");
|
|
267
|
+
auth
|
|
268
|
+
.command("login")
|
|
269
|
+
.description("Log in via an auth provider")
|
|
270
|
+
.option("--provider <id>", "skip the picker and use this provider id")
|
|
271
|
+
.action((o) => run2(async () => runAuthLogin(o.provider ? { provider: o.provider } : {}, await authDeps())));
|
|
272
|
+
auth
|
|
273
|
+
.command("logout")
|
|
274
|
+
.description("Clear the active session")
|
|
275
|
+
.action(() => run2(async () => runAuthLogout({}, await authDeps())));
|
|
276
|
+
auth
|
|
277
|
+
.command("status")
|
|
278
|
+
.description("Show the active session")
|
|
279
|
+
.option("--json", "machine-readable output")
|
|
280
|
+
.action((o) => run2(async () => runAuthStatus(o, await authDeps())));
|
|
212
281
|
if (opts.plugins) {
|
|
213
282
|
for (const plugin of opts.plugins) {
|
|
214
283
|
plugin.registerCommands?.(program);
|
package/dist/src/plugin-api.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { basicAuthPlugin } from "./core/auth/basic.js";
|
|
1
|
+
export { basicAuthPlugin, basicAuthProvider } from "./core/auth/basic.js";
|
|
2
2
|
export { loadConfig } from "./core/config.js";
|
|
3
3
|
export { createHttpClient } from "./core/http.js";
|
|
4
4
|
export { AuthError, ConfigError, HttpError } from "./errors.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dropsh",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2",
|
|
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": {
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
},
|
|
9
9
|
"exports": {
|
|
10
10
|
".": "./dist/src/index.js",
|
|
11
|
-
"./plugin": "./dist/src/plugin-api.js"
|
|
11
|
+
"./plugin": "./dist/src/plugin-api.js",
|
|
12
|
+
"./package.json": "./package.json"
|
|
12
13
|
},
|
|
13
14
|
"files": [
|
|
14
15
|
"bin",
|
|
@@ -16,20 +17,6 @@
|
|
|
16
17
|
"README.md",
|
|
17
18
|
"LICENSE"
|
|
18
19
|
],
|
|
19
|
-
"scripts": {
|
|
20
|
-
"build": "tsc",
|
|
21
|
-
"build:plugins": "npm --prefix plugins/schemata run build && npm --prefix plugins/oauth2 run build",
|
|
22
|
-
"dev": "tsx src/index.ts",
|
|
23
|
-
"test": "vitest run --passWithNoTests",
|
|
24
|
-
"test:watch": "vitest",
|
|
25
|
-
"test:integration": "npm run build && vitest run -c vitest.config.integration.ts",
|
|
26
|
-
"typecheck": "tsc --noEmit",
|
|
27
|
-
"lint": "biome check src/ tests/integrations/helpers/ tests/integrations/schema/ tests/integrations/*.ts",
|
|
28
|
-
"lint:fix": "biome check --write src/ tests/integrations/helpers/ tests/integrations/schema/ tests/integrations/*.ts",
|
|
29
|
-
"drupal:up": "bash tests/integrations/bin/drupal-up.sh",
|
|
30
|
-
"drupal:down": "bash tests/integrations/bin/drupal-down.sh",
|
|
31
|
-
"prepublishOnly": "npm run typecheck && npm run build"
|
|
32
|
-
},
|
|
33
20
|
"keywords": [
|
|
34
21
|
"drupal",
|
|
35
22
|
"drupal11",
|
|
@@ -56,9 +43,25 @@
|
|
|
56
43
|
"@types/node": "^20.14.0",
|
|
57
44
|
"tsx": "^4.19.0",
|
|
58
45
|
"typescript": "^5.6.0",
|
|
59
|
-
"
|
|
46
|
+
"vite": "^6.4.3",
|
|
47
|
+
"vitest": "^4.1.8"
|
|
60
48
|
},
|
|
61
49
|
"engines": {
|
|
62
50
|
"node": ">=20"
|
|
51
|
+
},
|
|
52
|
+
"scripts": {
|
|
53
|
+
"build": "tsc",
|
|
54
|
+
"build:packages": "pnpm --filter \"./packages/*\" run build",
|
|
55
|
+
"build:plugins": "pnpm --filter \"./plugins/*\" run build",
|
|
56
|
+
"dev": "tsx src/index.ts",
|
|
57
|
+
"test": "vitest run --passWithNoTests",
|
|
58
|
+
"test:watch": "vitest",
|
|
59
|
+
"test:integration": "pnpm run build && vitest run -c vitest.config.integration.ts",
|
|
60
|
+
"typecheck": "tsc --noEmit",
|
|
61
|
+
"lint": "biome check src/ tests/integrations/helpers/ tests/integrations/schema/ tests/integrations/*.ts",
|
|
62
|
+
"lint:fix": "biome check --write src/ tests/integrations/helpers/ tests/integrations/schema/ tests/integrations/*.ts",
|
|
63
|
+
"drupal:up": "bash tests/integrations/bin/drupal-up.sh",
|
|
64
|
+
"drupal:down": "bash tests/integrations/bin/drupal-down.sh",
|
|
65
|
+
"release": "pnpm publish --no-git-checks && pnpm -r publish --no-git-checks"
|
|
63
66
|
}
|
|
64
|
-
}
|
|
67
|
+
}
|