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.
- package/LICENSE +51 -0
- package/README.md +78 -0
- package/bin/dropsh +7 -0
- package/bin/dropsh-src +7 -0
- package/dist/src/commands/_data.js +19 -0
- package/dist/src/commands/create.js +14 -0
- package/dist/src/commands/delete.js +13 -0
- package/dist/src/commands/login.js +121 -0
- package/dist/src/commands/read.js +10 -0
- package/dist/src/commands/schema.js +61 -0
- package/dist/src/commands/search.js +30 -0
- package/dist/src/commands/update.js +20 -0
- package/dist/src/commands/upload-file.js +30 -0
- package/dist/src/core/auth/api-key.js +13 -0
- package/dist/src/core/auth/basic.js +24 -0
- package/dist/src/core/auth/factory.js +17 -0
- package/dist/src/core/auth/jwt.js +12 -0
- package/dist/src/core/auth/oauth2-authcode.js +67 -0
- package/dist/src/core/auth/oauth2.js +64 -0
- package/dist/src/core/auth/token-store.js +26 -0
- package/dist/src/core/auth/types.js +1 -0
- package/dist/src/core/cache/file-store.js +38 -0
- package/dist/src/core/cli/output.js +20 -0
- package/dist/src/core/config.js +36 -0
- package/dist/src/core/http.js +69 -0
- package/dist/src/core/jsonapi/client.js +46 -0
- package/dist/src/core/jsonapi/query.js +18 -0
- package/dist/src/core/plugin.js +1 -0
- package/dist/src/core/schema/catalog.js +29 -0
- package/dist/src/core/schema/jsonschema-source.js +46 -0
- package/dist/src/core/schema/sources/heuristic.js +100 -0
- package/dist/src/core/schema/to-jsonschema.js +51 -0
- package/dist/src/core/schema/validate.js +32 -0
- package/dist/src/errors.js +49 -0
- package/dist/src/index.js +244 -0
- package/dist/src/plugin-api.js +4 -0
- 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
|
+
}
|
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
|
+
}
|