dropsh 0.5.8 → 0.6.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/dist/src/index.js CHANGED
@@ -1,13 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import { parseArgs } from "node:util";
3
3
  import { Command } from "commander";
4
+ import { readDataArg } from "./commands/_data.js";
4
5
  import { runAuthLogin, runAuthLogout, runAuthStatus, runAuthUse } from "./commands/auth.js";
5
6
  import { runCreate } from "./commands/create.js";
6
7
  import { runDelete } from "./commands/delete.js";
7
8
  import { runRead } from "./commands/read.js";
8
9
  import { applyOperationSchemaPlugins, operationHookPluginIds, runSchema, SCHEMA_PIPELINE_VERSION, schemaCacheMetadataMatches, siteCacheRoot, } from "./commands/schema.js";
9
10
  import { runSearch } from "./commands/search.js";
10
- import { runUpdate } from "./commands/update.js";
11
+ import { assertUpdateTarget, runUpdate, } from "./commands/update.js";
11
12
  import { runUploadFile } from "./commands/upload-file.js";
12
13
  import { collectProviders, defaultProvider, providerById, sessionlessProvider, } from "./core/auth/registry.js";
13
14
  import { readProfiles, writeProfile } from "./core/auth/session-store.js";
@@ -17,10 +18,14 @@ import { createPrompt } from "./core/cli/prompt.js";
17
18
  import { loadConfig } from "./core/config.js";
18
19
  import { createHttpClient } from "./core/http.js";
19
20
  import { createJsonApiClient } from "./core/jsonapi/client.js";
21
+ import { buildPayloadFromParameters } from "./core/params/from-parameters.js";
22
+ import { describeFields, FIELD_PARAMETER_HELP, renderFieldsTable } from "./core/params/help.js";
23
+ import { parseFieldArgs } from "./core/params/parse-args.js";
24
+ import { composeRequestHooks } from "./core/request-hooks.js";
20
25
  import { fetchJsonSchema } from "./core/schema/jsonschema-source.js";
21
26
  import { toOperationVariant } from "./core/schema/to-jsonschema.js";
22
27
  import { validatePayload } from "./core/schema/validate.js";
23
- import { AuthError, ConfigError, exitCodeFor } from "./errors.js";
28
+ import { AuthError, ConfigError, exitCodeFor, ValidationError } from "./errors.js";
24
29
  function resolveConfigPath(override) {
25
30
  return override ?? process.env.DROPSH_CONFIG ?? "dropsh.config.js";
26
31
  }
@@ -119,11 +124,18 @@ async function defaultContext(configPath, profile) {
119
124
  now: Date.now,
120
125
  ...(profile !== undefined ? { profile } : {}),
121
126
  });
127
+ const pluginContext = {
128
+ http,
129
+ auth,
130
+ baseUrl: cfg.site.base_url,
131
+ jsonapiPrefix: cfg.site.jsonapi_prefix,
132
+ };
122
133
  const client = createJsonApiClient({
123
134
  baseUrl: cfg.site.base_url,
124
135
  prefix: cfg.site.jsonapi_prefix,
125
136
  http,
126
137
  auth,
138
+ alterRequest: composeRequestHooks(cfg.plugins, pluginContext),
127
139
  });
128
140
  return {
129
141
  client,
@@ -193,6 +205,73 @@ export function buildProgram(opts = {}) {
193
205
  throw new ConfigError(`Unknown format '${f}'. Available: ${["json", ...renderers.map((r) => r.id)].join(", ")}`);
194
206
  }
195
207
  }
208
+ /**
209
+ * Print a resolved schema as its field listing: a table for a human, or the
210
+ * same data verbatim on an explicit `--format json`. Shared by `create`,
211
+ * `update` and `schema`, so the three cannot drift.
212
+ */
213
+ function emitFieldListing(schema, rctx) {
214
+ const described = describeFields(schema);
215
+ if (program.getOptionValueSource("format") === "default") {
216
+ stdout(`${renderFieldsTable(described)}\n`);
217
+ }
218
+ else {
219
+ output.emit(described, rctx);
220
+ }
221
+ }
222
+ /**
223
+ * The write-command wiring `create` and `update` share: decide `--data` vs
224
+ * field parameters, fetch the operation schema at most once, build the payload
225
+ * from the parameters, and hand back the args/deps both `runCreate` and
226
+ * `runUpdate` accept. The two commands differ only in how they identify their
227
+ * target, which is why that part stays in each action.
228
+ */
229
+ async function writeWiring(input) {
230
+ const { ctx, options } = input;
231
+ // --fields is a discovery request, not a write: resolve the schema through
232
+ // the normal fetch-and-cache pipeline, print the parameters, send nothing.
233
+ // It reads as a table because a human asked what the fields are; an explicit
234
+ // --format json hands back the same data verbatim for a caller to consume.
235
+ if (options.fields === true) {
236
+ emitFieldListing(await loadOrFetchSchema(ctx, input.schemaTarget, input.operation), input.rctx);
237
+ return "listed";
238
+ }
239
+ // Exactly one route produces the document, and it is settled here so the
240
+ // command itself only ever receives a finished payload.
241
+ const fields = parseFieldArgs(input.tokens);
242
+ if (options.data !== undefined && fields.length > 0) {
243
+ throw new ValidationError("--data and field parameters are mutually exclusive; use one or the other");
244
+ }
245
+ if (options.data === undefined && fields.length === 0) {
246
+ throw new ValidationError("provide either --data or field parameters (--<field> <value>)");
247
+ }
248
+ let cached;
249
+ const schema = async () => (cached ??= await loadOrFetchSchema(ctx, input.schemaTarget, input.operation));
250
+ let payload;
251
+ if (options.data !== undefined) {
252
+ payload = await readDataArg(options.data);
253
+ }
254
+ else {
255
+ input.guard?.();
256
+ payload = buildPayloadFromParameters({
257
+ schema: await schema(),
258
+ parameters: fields,
259
+ operation: input.operation,
260
+ ...(input.id !== undefined ? { id: input.id } : {}),
261
+ resourceType: input.resourceType,
262
+ });
263
+ }
264
+ const args = { payload };
265
+ if (options.dryRun !== undefined)
266
+ args.dryRun = options.dryRun;
267
+ if (options.validate === false)
268
+ args.noValidate = true;
269
+ const deps = { client: ctx.client, emit: (v) => output.emit(v, input.rctx) };
270
+ if (!args.noValidate) {
271
+ deps.validate = async (payload, t) => validatePayload(await schema(), payload, t);
272
+ }
273
+ return { args, deps };
274
+ }
196
275
  function assertJsonOnly(command) {
197
276
  const f = currentFormat();
198
277
  if (f !== "json") {
@@ -339,54 +418,61 @@ Example:
339
418
  .command("create <entity_type>")
340
419
  .description("Create an entity of given type/bundle")
341
420
  .requiredOption("--bundle <bundle>")
342
- .requiredOption("--data <json>", "inline JSON or @path")
421
+ .option("--data <json>", "inline JSON or @path")
343
422
  .option("--dry-run")
344
423
  .option("--no-validate", "skip client-side schema validation")
345
- .action((entityType, o) => {
346
- const args = { entityType, bundle: o.bundle, dataArg: o.data };
347
- if (o.dryRun !== undefined)
348
- args.dryRun = o.dryRun;
349
- if (o.validate === false)
350
- args.noValidate = true;
424
+ .option("--fields", "list this bundle's field parameters and exit")
425
+ .allowUnknownOption()
426
+ .addHelpText("after", FIELD_PARAMETER_HELP)
427
+ .action((entityType, o, cmd) => {
351
428
  return run(async (ctx) => {
352
- const rctx = { command: "create", entityType, bundle: o.bundle };
353
- const deps = { client: ctx.client, emit: (v) => output.emit(v, rctx) };
354
- if (!args.noValidate) {
355
- deps.validate = async (payload, target) => {
356
- const schema = await loadOrFetchSchema(ctx, target, "create");
357
- validatePayload(schema, payload, target);
358
- };
359
- }
360
- await runCreate(args, deps);
429
+ const wiring = await writeWiring({
430
+ ctx,
431
+ operation: "create",
432
+ schemaTarget: `${entityType}/${o.bundle}`,
433
+ resourceType: `${entityType}--${o.bundle}`,
434
+ tokens: cmd.args.slice(1),
435
+ rctx: { command: "create", entityType, bundle: o.bundle },
436
+ options: o,
437
+ });
438
+ if (wiring === "listed")
439
+ return;
440
+ await runCreate({ entityType, bundle: o.bundle, ...wiring.args }, wiring.deps);
361
441
  }, assertRenderable);
362
442
  });
363
443
  program
364
444
  .command("update <target>")
365
445
  .description("Update an existing entity")
366
- .requiredOption("--data <json>", "inline JSON or @path")
446
+ .option("--data <json>", "inline JSON or @path")
367
447
  .option("--dry-run")
368
448
  .option("--no-validate", "skip client-side schema validation")
369
- .action((target, o) => {
370
- const args = { target, dataArg: o.data };
371
- if (o.dryRun !== undefined)
372
- args.dryRun = o.dryRun;
373
- if (o.validate === false)
374
- args.noValidate = true;
449
+ .option("--fields", "list this bundle's field parameters and exit")
450
+ .allowUnknownOption()
451
+ .addHelpText("after", FIELD_PARAMETER_HELP)
452
+ .action((target, o, cmd) => {
375
453
  return run(async (ctx) => {
376
- const [entityType, bundle] = target.split("/");
454
+ const [entityType, bundle, id] = target.split("/");
377
455
  const rctx = { command: "update", target };
378
456
  if (entityType !== undefined)
379
457
  rctx.entityType = entityType;
380
458
  if (bundle !== undefined)
381
459
  rctx.bundle = bundle;
382
- const deps = { client: ctx.client, emit: (v) => output.emit(v, rctx) };
383
- if (!args.noValidate) {
384
- deps.validate = async (payload, t) => {
385
- const schema = await loadOrFetchSchema(ctx, t, "update");
386
- validatePayload(schema, payload, t);
387
- };
388
- }
389
- await runUpdate(args, deps);
460
+ const wiring = await writeWiring({
461
+ ctx,
462
+ operation: "update",
463
+ schemaTarget: `${entityType}/${bundle}`,
464
+ resourceType: `${entityType}--${bundle}`,
465
+ tokens: cmd.args.slice(1),
466
+ rctx,
467
+ options: o,
468
+ ...(id !== undefined ? { id } : {}),
469
+ // Guard the target before building, so a malformed one is reported by
470
+ // its own message rather than as a missing data.id.
471
+ guard: () => assertUpdateTarget(target),
472
+ });
473
+ if (wiring === "listed")
474
+ return;
475
+ await runUpdate({ target, ...wiring.args }, wiring.deps);
390
476
  }, assertRenderable);
391
477
  });
392
478
  program
@@ -418,21 +504,29 @@ Example:
418
504
  .description("Catalog (no target) or JSON Schema for <entity>/<bundle>")
419
505
  .option("--for <op>", "create|update", "create")
420
506
  .option("--refresh", "bypass cache for this call")
507
+ .option("--fields", "list the target's field parameters instead of its schema")
421
508
  .action((target, o) => {
422
509
  const operation = o.for === "update" ? "update" : "create";
423
510
  const schemaArgs = target !== undefined
424
511
  ? { target, operation, refresh: Boolean(o.refresh) }
425
512
  : { operation, refresh: Boolean(o.refresh) };
426
- return run((ctx) => runSchema(schemaArgs, {
427
- http: ctx.http,
428
- auth: ctx.auth,
429
- baseUrl: ctx.baseUrl,
430
- jsonapiPrefix: ctx.jsonapiPrefix,
431
- cwd: ctx.cwd,
432
- emit: output.emit,
433
- warn: (m) => stderr(`${m}\n`),
434
- plugins: ctx.plugins,
435
- }), () => assertJsonOnly("schema"));
513
+ return run((ctx) => {
514
+ if (o.fields === true && target === undefined) {
515
+ throw new ValidationError("--fields needs a target: dropsh schema <entity_type>/<bundle> --fields");
516
+ }
517
+ // --fields is the same schema, viewed as its parameters: swapping the
518
+ // emitter inherits runSchema's caching and --refresh untouched.
519
+ return runSchema(schemaArgs, {
520
+ http: ctx.http,
521
+ auth: ctx.auth,
522
+ baseUrl: ctx.baseUrl,
523
+ jsonapiPrefix: ctx.jsonapiPrefix,
524
+ cwd: ctx.cwd,
525
+ emit: o.fields === true ? (schema) => emitFieldListing(schema) : output.emit,
526
+ warn: (m) => stderr(`${m}\n`),
527
+ plugins: ctx.plugins,
528
+ });
529
+ }, () => assertJsonOnly("schema"));
436
530
  });
437
531
  const auth = program.command("auth").description("Manage authentication");
438
532
  auth
@@ -7,12 +7,17 @@ export type { Config, SiteConfig } from "./core/config.js";
7
7
  export { loadConfig } from "./core/config.js";
8
8
  export type { HttpClient, HttpRequest } from "./core/http.js";
9
9
  export { createHttpClient } from "./core/http.js";
10
- export type { JsonApiClient, JsonApiOptions, ResourceWriteBody, } from "./core/jsonapi/client.js";
10
+ export type { DropSHOperation, JsonApiClient, JsonApiOptions, ResourceWriteBody, } from "./core/jsonapi/client.js";
11
11
  export { createJsonApiClient } from "./core/jsonapi/client.js";
12
12
  export type { Collection, FilterOp } from "./core/jsonapi/collection.js";
13
13
  export type { JsonApiResourceObject, Resource, } from "./core/jsonapi/resource.js";
14
14
  export type { JsonApiDocument, JsonApiResource } from "./core/jsonapi/types.js";
15
- export type { DropSHPlugin, PluginContext, PluginDescriptor, SchemaOperation, } from "./core/plugin.js";
15
+ export type { BuildPayloadInput } from "./core/params/from-parameters.js";
16
+ export { buildPayloadFromParameters } from "./core/params/from-parameters.js";
17
+ export type { RawParameter } from "./core/params/parse-args.js";
18
+ export type { FieldDescriptor, SchemaFieldIndex } from "./core/params/schema-fields.js";
19
+ export { indexSchemaFields, propertiesOf } from "./core/params/schema-fields.js";
20
+ export type { DropSHPlugin, PluginContext, PluginDescriptor, RequestContext, SchemaOperation, } from "./core/plugin.js";
16
21
  export { composePlugins } from "./core/plugin.js";
17
- export { AuthError, ConfigError, HttpError } from "./errors.js";
22
+ export { AuthError, ConfigError, HttpError, PluginError } from "./errors.js";
18
23
  export type { CommandContext } from "./index.js";
@@ -3,5 +3,7 @@ export { indexIncluded, isInteractive } from "./core/cli/render.js";
3
3
  export { loadConfig } from "./core/config.js";
4
4
  export { createHttpClient } from "./core/http.js";
5
5
  export { createJsonApiClient } from "./core/jsonapi/client.js";
6
+ export { buildPayloadFromParameters } from "./core/params/from-parameters.js";
7
+ export { indexSchemaFields, propertiesOf } from "./core/params/schema-fields.js";
6
8
  export { composePlugins } from "./core/plugin.js";
7
- export { AuthError, ConfigError, HttpError } from "./errors.js";
9
+ export { AuthError, ConfigError, HttpError, PluginError } from "./errors.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dropsh",
3
- "version": "0.5.8",
3
+ "version": "0.6.1",
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": {
@@ -66,8 +66,8 @@
66
66
  "test:watch": "vitest",
67
67
  "test:integration": "pnpm run build && pnpm run build:packages && vitest run -c vitest.config.integration.ts",
68
68
  "typecheck": "pnpm run build:packages && tsc --noEmit && pnpm -r run typecheck",
69
- "lint": "biome check src/ packages/ plugins/ tests/integrations/helpers/ tests/integrations/schema/ tests/integrations/*.ts",
70
- "lint:fix": "biome check --write src/ packages/ plugins/ tests/integrations/helpers/ tests/integrations/schema/ tests/integrations/*.ts",
69
+ "lint": "biome check src/ packages/ plugins/ tests/integrations/helpers/ tests/integrations/params/ tests/integrations/schema/ tests/integrations/*.ts",
70
+ "lint:fix": "biome check --write src/ packages/ plugins/ tests/integrations/helpers/ tests/integrations/params/ tests/integrations/schema/ tests/integrations/*.ts",
71
71
  "drupal:up": "bash tests/integrations/bin/drupal-up.sh",
72
72
  "drupal:down": "bash tests/integrations/bin/drupal-down.sh",
73
73
  "init-worktree": "node scripts/init-worktree.mjs",