@beomsukoh/zotero-cli 0.2.0
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 +21 -0
- package/README.md +117 -0
- package/dist/adapters/http/HttpZoteroAdapter.js +332 -0
- package/dist/adapters/output/ConsoleOutputAdapter.js +8 -0
- package/dist/application/errors.js +42 -0
- package/dist/application/ports.js +1 -0
- package/dist/application/types.js +1 -0
- package/dist/cli.js +11 -0
- package/dist/commands/CommandRegistry.js +44 -0
- package/dist/commands/attachCommand.js +49 -0
- package/dist/commands/collectionsCommand.js +52 -0
- package/dist/commands/createCollectionCommand.js +47 -0
- package/dist/commands/createItemCommand.js +91 -0
- package/dist/commands/deleteCommand.js +53 -0
- package/dist/commands/exportCommand.js +56 -0
- package/dist/commands/fulltextCommand.js +44 -0
- package/dist/commands/helpers.js +27 -0
- package/dist/commands/itemGetCommand.js +52 -0
- package/dist/commands/itemsCommand.js +62 -0
- package/dist/commands/librariesCommand.js +27 -0
- package/dist/commands/searchCommand.js +67 -0
- package/dist/commands/tagsCommand.js +39 -0
- package/dist/commands/templateCommand.js +38 -0
- package/dist/commands/types.js +1 -0
- package/dist/commands/typesInfoCommand.js +55 -0
- package/dist/compositionRoot.js +44 -0
- package/dist/infrastructure/httpClient.js +55 -0
- package/dist/utils/args.js +67 -0
- package/package.json +35 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { ValidationError } from "../application/errors.js";
|
|
2
|
+
import { assertNoMissingOptionValues, assertNoUnknownOptions, hasBoolean, parseArgs } from "../utils/args.js";
|
|
3
|
+
import { renderJson } from "./helpers.js";
|
|
4
|
+
export class TypesInfoCommand {
|
|
5
|
+
name = "types";
|
|
6
|
+
category = "Lookup";
|
|
7
|
+
description = "Show available item types, or fields for a specific type.";
|
|
8
|
+
help() {
|
|
9
|
+
return [
|
|
10
|
+
"Usage:",
|
|
11
|
+
" zt types",
|
|
12
|
+
" zt types <itemType>",
|
|
13
|
+
" zt types <itemType> --creators",
|
|
14
|
+
"",
|
|
15
|
+
"List all available item types, or show the fields (or creator types)",
|
|
16
|
+
"for a specific item type.",
|
|
17
|
+
"",
|
|
18
|
+
"Options:",
|
|
19
|
+
" --creators Show creator types instead of fields",
|
|
20
|
+
"",
|
|
21
|
+
"Examples:",
|
|
22
|
+
" zt types",
|
|
23
|
+
" zt types journalArticle",
|
|
24
|
+
" zt types journalArticle --creators"
|
|
25
|
+
].join("\n");
|
|
26
|
+
}
|
|
27
|
+
parse(argv) {
|
|
28
|
+
const parsed = parseArgs(argv);
|
|
29
|
+
assertNoUnknownOptions(parsed, ["creators"]);
|
|
30
|
+
assertNoMissingOptionValues(parsed, []);
|
|
31
|
+
if (parsed.positionals.length > 1) {
|
|
32
|
+
throw new ValidationError("types accepts at most one positional argument (item type).");
|
|
33
|
+
}
|
|
34
|
+
const itemType = parsed.positionals[0];
|
|
35
|
+
const creators = hasBoolean(parsed, "creators");
|
|
36
|
+
if (creators && !itemType) {
|
|
37
|
+
throw new ValidationError("--creators requires an item type. Usage: zt types <itemType> --creators");
|
|
38
|
+
}
|
|
39
|
+
return { itemType, creators };
|
|
40
|
+
}
|
|
41
|
+
async execute(input, context) {
|
|
42
|
+
if (!input.itemType) {
|
|
43
|
+
const result = await context.zotero.getItemTypes();
|
|
44
|
+
context.output.write(renderJson(result));
|
|
45
|
+
}
|
|
46
|
+
else if (input.creators) {
|
|
47
|
+
const result = await context.zotero.getItemTypeCreatorTypes(input.itemType);
|
|
48
|
+
context.output.write(renderJson(result));
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
const result = await context.zotero.getItemTypeFields(input.itemType);
|
|
52
|
+
context.output.write(renderJson(result));
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { NodeHttpClient } from "./infrastructure/httpClient.js";
|
|
2
|
+
import { HttpZoteroAdapter } from "./adapters/http/HttpZoteroAdapter.js";
|
|
3
|
+
import { ConsoleOutputAdapter } from "./adapters/output/ConsoleOutputAdapter.js";
|
|
4
|
+
import { CommandRegistry } from "./commands/CommandRegistry.js";
|
|
5
|
+
import { LibrariesCommand } from "./commands/librariesCommand.js";
|
|
6
|
+
import { CollectionsCommand } from "./commands/collectionsCommand.js";
|
|
7
|
+
import { ItemsCommand } from "./commands/itemsCommand.js";
|
|
8
|
+
import { ItemGetCommand } from "./commands/itemGetCommand.js";
|
|
9
|
+
import { SearchCommand } from "./commands/searchCommand.js";
|
|
10
|
+
import { TagsCommand } from "./commands/tagsCommand.js";
|
|
11
|
+
import { CreateCollectionCommand } from "./commands/createCollectionCommand.js";
|
|
12
|
+
import { CreateItemCommand } from "./commands/createItemCommand.js";
|
|
13
|
+
import { DeleteCommand } from "./commands/deleteCommand.js";
|
|
14
|
+
import { AttachCommand } from "./commands/attachCommand.js";
|
|
15
|
+
import { ExportCommand } from "./commands/exportCommand.js";
|
|
16
|
+
import { FulltextCommand } from "./commands/fulltextCommand.js";
|
|
17
|
+
import { TemplateCommand } from "./commands/templateCommand.js";
|
|
18
|
+
import { TypesInfoCommand } from "./commands/typesInfoCommand.js";
|
|
19
|
+
export function createRuntime() {
|
|
20
|
+
const config = {
|
|
21
|
+
apiKey: process.env["ZOTERO_API_KEY"],
|
|
22
|
+
userId: process.env["ZOTERO_USER_ID"] ?? "0",
|
|
23
|
+
baseUrl: process.env["ZOTERO_BASE_URL"] ?? "http://localhost:23119/api"
|
|
24
|
+
};
|
|
25
|
+
const http = new NodeHttpClient();
|
|
26
|
+
const zotero = new HttpZoteroAdapter(http, config.baseUrl, config.apiKey, config.userId);
|
|
27
|
+
const output = new ConsoleOutputAdapter();
|
|
28
|
+
const registry = new CommandRegistry();
|
|
29
|
+
registry.register(new LibrariesCommand());
|
|
30
|
+
registry.register(new CollectionsCommand());
|
|
31
|
+
registry.register(new ItemsCommand());
|
|
32
|
+
registry.register(new ItemGetCommand());
|
|
33
|
+
registry.register(new SearchCommand());
|
|
34
|
+
registry.register(new TagsCommand());
|
|
35
|
+
registry.register(new CreateCollectionCommand());
|
|
36
|
+
registry.register(new CreateItemCommand());
|
|
37
|
+
registry.register(new DeleteCommand());
|
|
38
|
+
registry.register(new AttachCommand());
|
|
39
|
+
registry.register(new ExportCommand());
|
|
40
|
+
registry.register(new FulltextCommand());
|
|
41
|
+
registry.register(new TemplateCommand());
|
|
42
|
+
registry.register(new TypesInfoCommand());
|
|
43
|
+
return { output, zotero, registry, config };
|
|
44
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export class NodeHttpClient {
|
|
2
|
+
async request(method, url, options = {}) {
|
|
3
|
+
const response = await fetch(url, {
|
|
4
|
+
method,
|
|
5
|
+
headers: options.headers,
|
|
6
|
+
body: options.body
|
|
7
|
+
});
|
|
8
|
+
const headers = {};
|
|
9
|
+
response.headers.forEach((value, key) => {
|
|
10
|
+
headers[key] = value;
|
|
11
|
+
});
|
|
12
|
+
return {
|
|
13
|
+
status: response.status,
|
|
14
|
+
headers,
|
|
15
|
+
body: await response.text()
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
async requestRaw(method, url, options = {}) {
|
|
19
|
+
const response = await fetch(url, {
|
|
20
|
+
method,
|
|
21
|
+
headers: options.headers,
|
|
22
|
+
});
|
|
23
|
+
const headers = {};
|
|
24
|
+
response.headers.forEach((value, key) => {
|
|
25
|
+
headers[key] = value;
|
|
26
|
+
});
|
|
27
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
28
|
+
return {
|
|
29
|
+
status: response.status,
|
|
30
|
+
headers,
|
|
31
|
+
data: Buffer.from(arrayBuffer),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
async requestFormData(url, fields, file) {
|
|
35
|
+
const formData = new FormData();
|
|
36
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
37
|
+
formData.append(key, value);
|
|
38
|
+
}
|
|
39
|
+
const blob = new Blob([file.data], { type: file.contentType });
|
|
40
|
+
formData.append("file", blob, file.name);
|
|
41
|
+
const response = await fetch(url, {
|
|
42
|
+
method: "POST",
|
|
43
|
+
body: formData,
|
|
44
|
+
});
|
|
45
|
+
const headers = {};
|
|
46
|
+
response.headers.forEach((value, key) => {
|
|
47
|
+
headers[key] = value;
|
|
48
|
+
});
|
|
49
|
+
return {
|
|
50
|
+
status: response.status,
|
|
51
|
+
headers,
|
|
52
|
+
body: await response.text(),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { ValidationError } from "../application/errors.js";
|
|
2
|
+
export function parseArgs(tokens) {
|
|
3
|
+
const positionals = [];
|
|
4
|
+
const options = new Map();
|
|
5
|
+
const booleans = new Set();
|
|
6
|
+
for (let index = 0; index < tokens.length; index += 1) {
|
|
7
|
+
const token = tokens[index];
|
|
8
|
+
if (!token)
|
|
9
|
+
continue;
|
|
10
|
+
if (!token.startsWith("--")) {
|
|
11
|
+
positionals.push(token);
|
|
12
|
+
continue;
|
|
13
|
+
}
|
|
14
|
+
const optionToken = token.slice(2);
|
|
15
|
+
if (!optionToken)
|
|
16
|
+
throw new ValidationError(`Invalid option: ${token}`);
|
|
17
|
+
const equalsIndex = optionToken.indexOf("=");
|
|
18
|
+
if (equalsIndex >= 0) {
|
|
19
|
+
const key = optionToken.slice(0, equalsIndex);
|
|
20
|
+
const value = optionToken.slice(equalsIndex + 1);
|
|
21
|
+
appendOption(options, key, value);
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
const next = tokens[index + 1];
|
|
25
|
+
if (!next || next.startsWith("--")) {
|
|
26
|
+
booleans.add(optionToken);
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
appendOption(options, optionToken, next);
|
|
30
|
+
index += 1;
|
|
31
|
+
}
|
|
32
|
+
return { positionals, options, booleans };
|
|
33
|
+
}
|
|
34
|
+
export function getOption(parsed, name) {
|
|
35
|
+
return parsed.options.get(name)?.at(-1);
|
|
36
|
+
}
|
|
37
|
+
export function getOptions(parsed, name) {
|
|
38
|
+
return parsed.options.get(name) ?? [];
|
|
39
|
+
}
|
|
40
|
+
export function hasBoolean(parsed, name) {
|
|
41
|
+
return parsed.booleans.has(name);
|
|
42
|
+
}
|
|
43
|
+
export function assertNoUnknownOptions(parsed, allowed) {
|
|
44
|
+
const allowedSet = new Set(allowed);
|
|
45
|
+
for (const key of parsed.options.keys()) {
|
|
46
|
+
if (!allowedSet.has(key))
|
|
47
|
+
throw new ValidationError(`Unknown option: --${key}`);
|
|
48
|
+
}
|
|
49
|
+
for (const key of parsed.booleans) {
|
|
50
|
+
if (!allowedSet.has(key))
|
|
51
|
+
throw new ValidationError(`Unknown option: --${key}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
export function assertNoMissingOptionValues(parsed, optionNames) {
|
|
55
|
+
for (const name of optionNames) {
|
|
56
|
+
if (parsed.booleans.has(name))
|
|
57
|
+
throw new ValidationError(`Option --${name} requires a value.`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function appendOption(map, key, value) {
|
|
61
|
+
const existing = map.get(key);
|
|
62
|
+
if (!existing) {
|
|
63
|
+
map.set(key, [value]);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
existing.push(value);
|
|
67
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@beomsukoh/zotero-cli",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Pure CLI wrapper for the Zotero API.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"zt": "dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"README.md",
|
|
12
|
+
"LICENSE"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "node ./scripts/build.mjs",
|
|
16
|
+
"check": "tsc --noEmit && node ./scripts/validate-docs.mjs",
|
|
17
|
+
"dt": "tsx ./src/cli.ts",
|
|
18
|
+
"test": "vitest run",
|
|
19
|
+
"test:smoke": "vitest run --config vitest.config.smoke.ts",
|
|
20
|
+
"release:check": "pnpm check && pnpm test && pnpm build && pnpm pack:check",
|
|
21
|
+
"pack:check": "pnpm build && npm pack --dry-run",
|
|
22
|
+
"prepack": "pnpm build",
|
|
23
|
+
"prepublishOnly": "pnpm check && pnpm test && pnpm pack:check"
|
|
24
|
+
},
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=20"
|
|
27
|
+
},
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/node": "^22.0.0",
|
|
31
|
+
"tsx": "^4.0.0",
|
|
32
|
+
"typescript": "^5.7.0",
|
|
33
|
+
"vitest": "^4.0.0"
|
|
34
|
+
}
|
|
35
|
+
}
|