dropsh 0.0.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.
Files changed (37) hide show
  1. package/LICENSE +51 -0
  2. package/README.md +78 -0
  3. package/bin/dropsh +7 -0
  4. package/bin/dropsh-src +7 -0
  5. package/dist/src/commands/_data.js +19 -0
  6. package/dist/src/commands/create.js +14 -0
  7. package/dist/src/commands/delete.js +13 -0
  8. package/dist/src/commands/login.js +121 -0
  9. package/dist/src/commands/read.js +10 -0
  10. package/dist/src/commands/schema.js +61 -0
  11. package/dist/src/commands/search.js +30 -0
  12. package/dist/src/commands/update.js +20 -0
  13. package/dist/src/commands/upload-file.js +30 -0
  14. package/dist/src/core/auth/api-key.js +13 -0
  15. package/dist/src/core/auth/basic.js +24 -0
  16. package/dist/src/core/auth/factory.js +17 -0
  17. package/dist/src/core/auth/jwt.js +12 -0
  18. package/dist/src/core/auth/oauth2-authcode.js +67 -0
  19. package/dist/src/core/auth/oauth2.js +64 -0
  20. package/dist/src/core/auth/token-store.js +26 -0
  21. package/dist/src/core/auth/types.js +1 -0
  22. package/dist/src/core/cache/file-store.js +38 -0
  23. package/dist/src/core/cli/output.js +20 -0
  24. package/dist/src/core/config.js +36 -0
  25. package/dist/src/core/http.js +69 -0
  26. package/dist/src/core/jsonapi/client.js +46 -0
  27. package/dist/src/core/jsonapi/query.js +18 -0
  28. package/dist/src/core/plugin.js +1 -0
  29. package/dist/src/core/schema/catalog.js +29 -0
  30. package/dist/src/core/schema/jsonschema-source.js +46 -0
  31. package/dist/src/core/schema/sources/heuristic.js +100 -0
  32. package/dist/src/core/schema/to-jsonschema.js +51 -0
  33. package/dist/src/core/schema/validate.js +32 -0
  34. package/dist/src/errors.js +49 -0
  35. package/dist/src/index.js +244 -0
  36. package/dist/src/plugin-api.js +4 -0
  37. package/package.json +64 -0
@@ -0,0 +1,244 @@
1
+ #!/usr/bin/env node
2
+ import { parseArgs } from "node:util";
3
+ import { Command } from "commander";
4
+ import { runCreate } from "./commands/create.js";
5
+ import { runDelete } from "./commands/delete.js";
6
+ import { runRead } from "./commands/read.js";
7
+ import { runSchema } from "./commands/schema.js";
8
+ import { runSearch } from "./commands/search.js";
9
+ import { runUpdate } from "./commands/update.js";
10
+ import { runUploadFile } from "./commands/upload-file.js";
11
+ import { createFileStore } from "./core/cache/file-store.js";
12
+ import { createOutput } from "./core/cli/output.js";
13
+ import { loadConfig } from "./core/config.js";
14
+ import { createHttpClient } from "./core/http.js";
15
+ import { createJsonApiClient } from "./core/jsonapi/client.js";
16
+ import { fetchJsonSchema } from "./core/schema/jsonschema-source.js";
17
+ import { toOperationVariant } from "./core/schema/to-jsonschema.js";
18
+ import { validatePayload } from "./core/schema/validate.js";
19
+ import { ConfigError, exitCodeFor } from "./errors.js";
20
+ function resolveConfigPath(override) {
21
+ return override ?? process.env.DROPSH_CONFIG ?? "dropsh.config.js";
22
+ }
23
+ async function defaultContext(configPath) {
24
+ const cfg = await loadConfig(configPath);
25
+ const http = createHttpClient({ timeoutMs: cfg.defaults.timeout_ms });
26
+ const authPlugin = cfg.plugins.find((p) => p.createAuthAdapter);
27
+ if (!authPlugin?.createAuthAdapter) {
28
+ throw new ConfigError("No auth plugin configured. Add basicAuthPlugin() or oauth2Plugin() to config.plugins.");
29
+ }
30
+ const auth = authPlugin.createAuthAdapter();
31
+ const client = createJsonApiClient({
32
+ baseUrl: cfg.site.base_url,
33
+ prefix: cfg.site.jsonapi_prefix,
34
+ http,
35
+ auth,
36
+ });
37
+ return {
38
+ client,
39
+ http,
40
+ auth,
41
+ baseUrl: cfg.site.base_url,
42
+ jsonapiPrefix: cfg.site.jsonapi_prefix,
43
+ cwd: process.cwd(),
44
+ plugins: cfg.plugins,
45
+ };
46
+ }
47
+ export function buildProgram(opts = {}) {
48
+ const program = new Command();
49
+ program
50
+ .name("dropsh")
51
+ .description("Entity-agnostic CLI for Drupal 11 JSON:API")
52
+ .version("0.0.0")
53
+ .option("--config <path>", "path to config file (overrides DROPSH_CONFIG)");
54
+ const stdout = opts.stdout ?? ((s) => process.stdout.write(s));
55
+ const stderr = opts.stderr ?? ((s) => process.stderr.write(s));
56
+ const setExitCode = opts.setExitCode ??
57
+ ((c) => {
58
+ process.exitCode = c;
59
+ });
60
+ const contextFactory = opts.contextFactory ??
61
+ (() => defaultContext(resolveConfigPath(program.opts().config)));
62
+ const output = createOutput({ stdout, stderr });
63
+ async function run(fn) {
64
+ try {
65
+ const ctx = await contextFactory();
66
+ await fn(ctx);
67
+ }
68
+ catch (err) {
69
+ output.fail(err);
70
+ setExitCode(exitCodeFor(err));
71
+ }
72
+ }
73
+ async function loadOrFetchSchema(ctx, target, op) {
74
+ const store = createFileStore({
75
+ rootDir: `${ctx.cwd}/.dropsh/cache`,
76
+ warn: (m) => stderr(`${m}\n`),
77
+ });
78
+ const [entity, bundle] = target.split("/", 2);
79
+ const key = `schema/${entity}--${bundle}.${op}.json`;
80
+ const hit = await store.read(key);
81
+ if (hit !== undefined)
82
+ return hit;
83
+ const { schema: raw, source } = await fetchJsonSchema({
84
+ http: ctx.http,
85
+ auth: ctx.auth,
86
+ baseUrl: ctx.baseUrl,
87
+ jsonapiPrefix: ctx.jsonapiPrefix,
88
+ entity,
89
+ bundle,
90
+ warn: (m) => stderr(`${m}\n`),
91
+ plugins: ctx.plugins,
92
+ });
93
+ const transformed = toOperationVariant(raw, op);
94
+ const tagged = {
95
+ ...transformed,
96
+ "x-dropsh-source": source,
97
+ "x-dropsh-target": { entity_type: entity, bundle },
98
+ "x-dropsh-operation": op,
99
+ };
100
+ await store.write(key, tagged);
101
+ return tagged;
102
+ }
103
+ program
104
+ .command("read <target>")
105
+ .description("Read entity_type/bundle/uuid")
106
+ .action((target) => run((ctx) => runRead({ target }, { client: ctx.client, emit: output.emit })));
107
+ program
108
+ .command("search <entity_type>")
109
+ .description("Search entities with filters")
110
+ .option("--bundle <bundle>")
111
+ .option("--filter <kv...>", "filter in key:value or key:op:value form", [])
112
+ .option("--limit <n>", "max results", (v) => parseInt(v, 10), 50)
113
+ .action((entityType, o) => {
114
+ // biome-ignore lint/suspicious/noExplicitAny: optional bundle added conditionally
115
+ const args = { entityType, filters: o.filter, limit: o.limit };
116
+ if (o.bundle !== undefined)
117
+ args.bundle = o.bundle;
118
+ run((ctx) => runSearch(args, { client: ctx.client, emit: output.emit }));
119
+ });
120
+ program
121
+ .command("create <entity_type>")
122
+ .description("Create an entity of given type/bundle")
123
+ .requiredOption("--bundle <bundle>")
124
+ .requiredOption("--data <json>", "inline JSON or @path")
125
+ .option("--dry-run")
126
+ .option("--no-validate", "skip client-side schema validation")
127
+ .action((entityType, o) => {
128
+ const args = { entityType, bundle: o.bundle, dataArg: o.data };
129
+ if (o.dryRun !== undefined)
130
+ args.dryRun = o.dryRun;
131
+ if (o.validate === false)
132
+ args.noValidate = true;
133
+ run(async (ctx) => {
134
+ const deps = { client: ctx.client, emit: output.emit };
135
+ if (!args.noValidate) {
136
+ deps.validate = async (payload, target) => {
137
+ const schema = await loadOrFetchSchema(ctx, target, "create");
138
+ validatePayload(schema, payload, target);
139
+ };
140
+ }
141
+ await runCreate(args, deps);
142
+ });
143
+ });
144
+ program
145
+ .command("update <target>")
146
+ .description("Update an existing entity")
147
+ .requiredOption("--data <json>", "inline JSON or @path")
148
+ .option("--dry-run")
149
+ .option("--no-validate", "skip client-side schema validation")
150
+ .action((target, o) => {
151
+ const args = { target, dataArg: o.data };
152
+ if (o.dryRun !== undefined)
153
+ args.dryRun = o.dryRun;
154
+ if (o.validate === false)
155
+ args.noValidate = true;
156
+ run(async (ctx) => {
157
+ const deps = { client: ctx.client, emit: output.emit };
158
+ if (!args.noValidate) {
159
+ deps.validate = async (payload, t) => {
160
+ const schema = await loadOrFetchSchema(ctx, t, "update");
161
+ validatePayload(schema, payload, t);
162
+ };
163
+ }
164
+ await runUpdate(args, deps);
165
+ });
166
+ });
167
+ program
168
+ .command("delete <target>")
169
+ .description("Delete an entity")
170
+ .option("--dry-run")
171
+ .action((target, o) => {
172
+ // biome-ignore lint/suspicious/noExplicitAny: optional dryRun added conditionally
173
+ const args = { target };
174
+ if (o.dryRun !== undefined)
175
+ args.dryRun = o.dryRun;
176
+ run((ctx) => runDelete(args, { client: ctx.client, emit: output.emit }));
177
+ });
178
+ program
179
+ .command("upload-file")
180
+ .description("Upload a file to an entity's file/media field")
181
+ .requiredOption("--target <target>", "entity_type/bundle/uuid/field_name")
182
+ .requiredOption("--file <path>")
183
+ .option("--dry-run")
184
+ .action((o) => {
185
+ // biome-ignore lint/suspicious/noExplicitAny: optional dryRun added conditionally
186
+ const args = { target: o.target, file: o.file };
187
+ if (o.dryRun !== undefined)
188
+ args.dryRun = o.dryRun;
189
+ run((ctx) => runUploadFile(args, { client: ctx.client, emit: output.emit }));
190
+ });
191
+ program
192
+ .command("schema [target]")
193
+ .description("Catalog (no target) or JSON Schema for <entity>/<bundle>")
194
+ .option("--for <op>", "create|update", "create")
195
+ .option("--refresh", "bypass cache for this call")
196
+ .action((target, o) => {
197
+ const operation = o.for === "update" ? "update" : "create";
198
+ const schemaArgs = target !== undefined
199
+ ? { target, operation, refresh: Boolean(o.refresh) }
200
+ : { operation, refresh: Boolean(o.refresh) };
201
+ run((ctx) => runSchema(schemaArgs, {
202
+ http: ctx.http,
203
+ auth: ctx.auth,
204
+ baseUrl: ctx.baseUrl,
205
+ jsonapiPrefix: ctx.jsonapiPrefix,
206
+ cwd: ctx.cwd,
207
+ emit: output.emit,
208
+ warn: (m) => stderr(`${m}\n`),
209
+ plugins: ctx.plugins,
210
+ }));
211
+ });
212
+ if (opts.plugins) {
213
+ for (const plugin of opts.plugins) {
214
+ plugin.registerCommands?.(program);
215
+ }
216
+ }
217
+ program.exitOverride();
218
+ return program;
219
+ }
220
+ function earlyConfigArg(argv) {
221
+ const { values } = parseArgs({
222
+ args: argv.slice(2),
223
+ options: { config: { type: "string" } },
224
+ strict: false,
225
+ });
226
+ return typeof values.config === "string" ? values.config : undefined;
227
+ }
228
+ export async function main() {
229
+ const configPath = resolveConfigPath(earlyConfigArg(process.argv));
230
+ const cfg = await loadConfig(configPath).catch(() => null);
231
+ const plugins = cfg?.plugins ?? [];
232
+ const program = buildProgram({ plugins });
233
+ try {
234
+ await program.parseAsync(process.argv);
235
+ }
236
+ catch (err) {
237
+ if (err.code === "commander.helpDisplayed")
238
+ return;
239
+ process.exitCode = process.exitCode ?? 1;
240
+ }
241
+ }
242
+ if (import.meta.url === `file://${process.argv[1]}`) {
243
+ main();
244
+ }
@@ -0,0 +1,4 @@
1
+ export { basicAuthPlugin } from "./core/auth/basic.js";
2
+ export { loadConfig } from "./core/config.js";
3
+ export { createHttpClient } from "./core/http.js";
4
+ export { AuthError, ConfigError, HttpError } from "./errors.js";
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "dropsh",
3
+ "version": "0.0.1",
4
+ "description": "Entity-agnostic helper CLI for Drupal 11 JSON:API, used by the dropsh Claude skill for editorial publishing workflows.",
5
+ "type": "module",
6
+ "bin": {
7
+ "dropsh": "./bin/dropsh"
8
+ },
9
+ "exports": {
10
+ ".": "./dist/src/index.js",
11
+ "./plugin": "./dist/src/plugin-api.js"
12
+ },
13
+ "files": [
14
+ "bin",
15
+ "dist/src",
16
+ "README.md",
17
+ "LICENSE"
18
+ ],
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
+ "keywords": [
34
+ "drupal",
35
+ "drupal11",
36
+ "jsonapi",
37
+ "cli",
38
+ "claude",
39
+ "claude-skill",
40
+ "editorial"
41
+ ],
42
+ "author": "Christian Wiedemann <gitlab@keytec.de>",
43
+ "license": "(MIT OR GPL-2.0-or-later)",
44
+ "repository": {
45
+ "type": "git",
46
+ "url": "git+https://github.com/pen-drop/dropsh.git"
47
+ },
48
+ "dependencies": {
49
+ "ajv": "^8.18.0",
50
+ "commander": "^12.1.0",
51
+ "drupal-jsonapi-params": "^3.0.1",
52
+ "open": "^11.0.0"
53
+ },
54
+ "devDependencies": {
55
+ "@biomejs/biome": "^2.4.14",
56
+ "@types/node": "^20.14.0",
57
+ "tsx": "^4.19.0",
58
+ "typescript": "^5.6.0",
59
+ "vitest": "^2.1.0"
60
+ },
61
+ "engines": {
62
+ "node": ">=20"
63
+ }
64
+ }