dropsh 0.1.1 → 0.2.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/bin/dropsh-src CHANGED
File without changes
@@ -7,3 +7,16 @@ export function loginCapableProviders(providers) {
7
7
  export function providerById(providers, id) {
8
8
  return providers.find((p) => p.id === id);
9
9
  }
10
+ /**
11
+ * A session-less provider (e.g. static Basic) is the sole configured provider
12
+ * that declares no login capability: it carries credentials inline and needs no
13
+ * stored session. Used by both resolveAuth and authStatus.
14
+ */
15
+ export function sessionlessProvider(providers) {
16
+ if (providers.length !== 1)
17
+ return undefined;
18
+ const only = providers[0];
19
+ if (!only)
20
+ return undefined;
21
+ return only.capabilities.login === false ? only : undefined;
22
+ }
@@ -1,3 +1,6 @@
1
+ import { createCollection } from "./collection.js";
2
+ import { toResource } from "./resource.js";
3
+ import { resolveType } from "./types.js";
1
4
  function joinUrl(base, prefix, path) {
2
5
  const b = base.replace(/\/+$/, "");
3
6
  const p = prefix.startsWith("/") ? prefix : `/${prefix}`;
@@ -22,7 +25,7 @@ export function createJsonApiClient(opts) {
22
25
  return { ok: true };
23
26
  return JSON.parse(res.body);
24
27
  }
25
- return {
28
+ const client = {
26
29
  async get(path, params) {
27
30
  const qs = params ? `?${params.getQueryString()}` : "";
28
31
  return send("GET", joinUrl(opts.baseUrl, opts.prefix, path) + qs);
@@ -42,5 +45,46 @@ export function createJsonApiClient(opts) {
42
45
  "Content-Disposition": `file; filename="${filename}"`,
43
46
  });
44
47
  },
48
+ collection(type) {
49
+ return createCollection(client, type);
50
+ },
51
+ async resource(type, id) {
52
+ const { path } = resolveType(type);
53
+ const doc = (await client.get(`${path}/${id}`));
54
+ return toResource(doc.data);
55
+ },
56
+ async create(type, body) {
57
+ const { path, type: resourceType } = resolveType(type);
58
+ const doc = (await client.post(path, {
59
+ data: { type: resourceType, ...body },
60
+ }));
61
+ return toResource(doc.data);
62
+ },
63
+ async update(type, id, body) {
64
+ const { path, type: resourceType } = resolveType(type);
65
+ const doc = (await client.patch(`${path}/${id}`, {
66
+ data: { type: resourceType, id, ...body },
67
+ }));
68
+ return toResource(doc.data);
69
+ },
70
+ async upsert(type, match, body) {
71
+ const existing = await client.collection(type).where(match.path, "=", match.value).first();
72
+ if (existing)
73
+ return client.update(type, existing.id, body);
74
+ return client.create(type, body);
75
+ },
76
+ async me() {
77
+ const doc = (await client.get(""));
78
+ const me = doc?.meta?.links?.me;
79
+ if (me?.meta?.id)
80
+ return me.meta.id;
81
+ if (me?.href) {
82
+ const segment = me.href.split("/").filter(Boolean).pop();
83
+ if (segment)
84
+ return segment;
85
+ }
86
+ throw new Error("Unable to resolve current user uuid from root document meta.links.me");
87
+ },
45
88
  };
89
+ return client;
46
90
  }
@@ -0,0 +1,49 @@
1
+ import { DrupalJsonApiParams } from "drupal-jsonapi-params";
2
+ import { toResource } from "./resource.js";
3
+ import { resolveType } from "./types.js";
4
+ export function createCollection(client, spec, params = new DrupalJsonApiParams()) {
5
+ const resolved = resolveType(spec);
6
+ const collection = {
7
+ where(path, op, value) {
8
+ if (op === "=")
9
+ params.addFilter(path, String(value));
10
+ else
11
+ params.addFilter(path, String(value), op);
12
+ return collection;
13
+ },
14
+ whereIn(path, values) {
15
+ params.addFilter(path, values.map(String), "IN");
16
+ return collection;
17
+ },
18
+ notExists(path) {
19
+ params.addFilter(path, null, "IS NULL");
20
+ return collection;
21
+ },
22
+ fields(names) {
23
+ params.addFields(resolved.type, names);
24
+ return collection;
25
+ },
26
+ sort(path, dir) {
27
+ params.addSort(path, dir);
28
+ return collection;
29
+ },
30
+ page(limit) {
31
+ params.addPageLimit(limit);
32
+ return collection;
33
+ },
34
+ async list() {
35
+ const doc = (await client.get(resolved.path, params));
36
+ const data = doc?.data ?? [];
37
+ return data.map(toResource);
38
+ },
39
+ async first() {
40
+ const results = await collection.page(1).list();
41
+ return results[0] ?? null;
42
+ },
43
+ async count() {
44
+ const results = await collection.fields([]).list();
45
+ return results.length;
46
+ },
47
+ };
48
+ return collection;
49
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Ergonomic accessors over a raw JSON:API resource object.
3
+ *
4
+ * A {@link Resource} wraps the `data` member of a JSON:API document and exposes
5
+ * typed helpers for attributes and relationships so callers stop digging through
6
+ * nested `attributes`/`relationships` structures by hand.
7
+ */
8
+ export function toResource(obj) {
9
+ return {
10
+ id: obj.id,
11
+ type: obj.type,
12
+ raw: obj,
13
+ attr(key) {
14
+ return obj.attributes?.[key];
15
+ },
16
+ rel(key) {
17
+ const data = obj.relationships?.[key]?.data;
18
+ if (data === undefined || data === null)
19
+ return null;
20
+ if (Array.isArray(data))
21
+ return data[0]?.id ?? null;
22
+ return data.id;
23
+ },
24
+ rels(key) {
25
+ const data = obj.relationships?.[key]?.data;
26
+ if (data === undefined || data === null)
27
+ return [];
28
+ if (Array.isArray(data))
29
+ return data.map((d) => d.id);
30
+ return [data.id];
31
+ },
32
+ };
33
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * JSON:API resource type/path normalization.
3
+ *
4
+ * Drupal JSON:API addresses resources by a `entity/bundle` path (e.g.
5
+ * `node/article`) but identifies them in document envelopes by a `entity--bundle`
6
+ * type (e.g. `node--article`). This helper accepts either form plus a convenient
7
+ * single-segment shorthand and produces both representations.
8
+ */
9
+ /**
10
+ * Resolve a loose type specifier into its `{ path, type }` pair.
11
+ *
12
+ * Accepted forms:
13
+ * - `'gaia_run'` -> `{ path: 'gaia_run/gaia_run', type: 'gaia_run--gaia_run' }`
14
+ * - `'entity/bundle'` -> `{ path: 'entity/bundle', type: 'entity--bundle' }`
15
+ * - `'entity--bundle'` -> `{ path: 'entity/bundle', type: 'entity--bundle' }`
16
+ */
17
+ export function resolveType(spec) {
18
+ if (spec.includes("--")) {
19
+ return { type: spec, path: spec.replace(/--/g, "/") };
20
+ }
21
+ if (spec.includes("/")) {
22
+ return { path: spec, type: spec.replace(/\//g, "--") };
23
+ }
24
+ return { path: `${spec}/${spec}`, type: `${spec}--${spec}` };
25
+ }
package/dist/src/index.js CHANGED
@@ -9,7 +9,7 @@ import { applyOperationSchemaPlugins, operationHookPluginIds, runSchema, SCHEMA_
9
9
  import { runSearch } from "./commands/search.js";
10
10
  import { runUpdate } from "./commands/update.js";
11
11
  import { runUploadFile } from "./commands/upload-file.js";
12
- import { collectProviders, providerById } from "./core/auth/registry.js";
12
+ import { collectProviders, providerById, sessionlessProvider } from "./core/auth/registry.js";
13
13
  import { readSession, writeSession } from "./core/auth/session-store.js";
14
14
  import { createFileStore } from "./core/cache/file-store.js";
15
15
  import { createOutput } from "./core/cli/output.js";
@@ -25,10 +25,20 @@ function resolveConfigPath(override) {
25
25
  return override ?? process.env.DROPSH_CONFIG ?? "dropsh.config.js";
26
26
  }
27
27
  export async function resolveAuth(deps) {
28
+ const providers = collectProviders(deps.plugins);
28
29
  const rec = await readSession(deps.baseUrl, deps.stateDir);
29
- if (!rec)
30
+ if (!rec) {
31
+ const sessionless = sessionlessProvider(providers);
32
+ if (sessionless) {
33
+ return sessionless.createAdapter(undefined, {
34
+ http: deps.http,
35
+ now: deps.now,
36
+ save: async () => { },
37
+ });
38
+ }
30
39
  throw new AuthError("Not authenticated. Run 'dropsh auth login'.");
31
- const provider = providerById(collectProviders(deps.plugins), rec.activeProvider);
40
+ }
41
+ const provider = providerById(providers, rec.activeProvider);
32
42
  if (!provider)
33
43
  throw new ConfigError(`active provider '${rec.activeProvider}' is not configured`);
34
44
  return provider.createAdapter(rec.session, {
@@ -37,6 +47,20 @@ export async function resolveAuth(deps) {
37
47
  save: (session) => writeSession(deps.baseUrl, provider.id, session, deps.stateDir),
38
48
  });
39
49
  }
50
+ export async function authStatus(deps) {
51
+ const providers = collectProviders(deps.plugins);
52
+ const rec = await readSession(deps.baseUrl, deps.stateDir);
53
+ if (!rec) {
54
+ const sessionless = sessionlessProvider(providers);
55
+ return sessionless
56
+ ? { loggedIn: true, provider: sessionless.id, sessionless: true }
57
+ : { loggedIn: false };
58
+ }
59
+ const provider = providerById(providers, rec.activeProvider);
60
+ if (!provider)
61
+ return { loggedIn: false };
62
+ return provider.status(rec.session);
63
+ }
40
64
  async function defaultContext(configPath) {
41
65
  const cfg = await loadConfig(configPath);
42
66
  const http = createHttpClient({ timeoutMs: cfg.defaults.timeout_ms });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dropsh",
3
- "version": "0.1.1",
3
+ "version": "0.2.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": {
@@ -17,6 +17,22 @@
17
17
  "README.md",
18
18
  "LICENSE"
19
19
  ],
20
+ "scripts": {
21
+ "build": "tsc",
22
+ "build:packages": "pnpm --filter \"./packages/*\" run build",
23
+ "build:plugins": "pnpm --filter \"./plugins/*\" run build",
24
+ "dev": "tsx src/index.ts",
25
+ "test": "vitest run --passWithNoTests",
26
+ "test:watch": "vitest",
27
+ "test:integration": "pnpm run build && pnpm run build:packages && vitest run -c vitest.config.integration.ts",
28
+ "typecheck": "pnpm run build:packages && tsc --noEmit && pnpm -r run typecheck",
29
+ "lint": "biome check src/ packages/ plugins/ tests/integrations/helpers/ tests/integrations/schema/ tests/integrations/*.ts",
30
+ "lint:fix": "biome check --write src/ packages/ plugins/ tests/integrations/helpers/ tests/integrations/schema/ tests/integrations/*.ts",
31
+ "drupal:up": "bash tests/integrations/bin/drupal-up.sh",
32
+ "drupal:down": "bash tests/integrations/bin/drupal-down.sh",
33
+ "prepublishOnly": "pnpm run typecheck && pnpm run build",
34
+ "release": "node scripts/release.mjs"
35
+ },
20
36
  "keywords": [
21
37
  "drupal",
22
38
  "drupal11",
@@ -46,22 +62,19 @@
46
62
  "vite": "^6.4.3",
47
63
  "vitest": "^4.1.8"
48
64
  },
65
+ "pnpm": {
66
+ "onlyBuiltDependencies": [
67
+ "esbuild"
68
+ ],
69
+ "overrides": {
70
+ "fast-uri": "^3.1.2",
71
+ "qs": "^6.15.2",
72
+ "vite": "^6.4.2",
73
+ "esbuild": "^0.25.0"
74
+ }
75
+ },
49
76
  "engines": {
50
77
  "node": ">=20"
51
78
  },
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 && pnpm run build:packages && vitest run -c vitest.config.integration.ts",
60
- "typecheck": "pnpm run build:packages && tsc --noEmit && pnpm -r run typecheck",
61
- "lint": "biome check src/ packages/ plugins/ tests/integrations/helpers/ tests/integrations/schema/ tests/integrations/*.ts",
62
- "lint:fix": "biome check --write src/ packages/ plugins/ 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 -r --include-workspace-root publish --no-git-checks"
66
- }
67
- }
79
+ "packageManager": "pnpm@10.29.1"
80
+ }