@ncukondo/search-hub 0.20.1 → 0.21.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/README.md +24 -22
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +2 -0
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/query/init.d.ts +20 -9
- package/dist/cli/commands/query/init.d.ts.map +1 -1
- package/dist/cli/commands/query/init.js +25 -10
- package/dist/cli/commands/query/init.js.map +1 -1
- package/dist/cli/commands/query/resolve.d.ts +5 -0
- package/dist/cli/commands/query/resolve.d.ts.map +1 -0
- package/dist/cli/commands/query/resolve.js +59 -0
- package/dist/cli/commands/query/resolve.js.map +1 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +46 -25
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/suggestions/rules.d.ts.map +1 -1
- package/dist/cli/suggestions/rules.js +21 -9
- package/dist/cli/suggestions/rules.js.map +1 -1
- package/dist/cli/suggestions/types.d.ts +2 -0
- package/dist/cli/suggestions/types.d.ts.map +1 -1
- package/dist/node_modules/dom-serializer/lib/index.js +1 -1
- package/dist/node_modules/domelementtype/lib/index.js +1 -1
- package/dist/node_modules/nth-check/lib/index.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -44,14 +44,14 @@ This creates config and data directories in platform-specific locations:
|
|
|
44
44
|
|
|
45
45
|
2. Create a query file:
|
|
46
46
|
```bash
|
|
47
|
-
search-hub query init
|
|
47
|
+
search-hub query init "my review"
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
-
This
|
|
50
|
+
This creates `queries/my-review.yaml` with JSON Schema support for editor autocompletion. Edit it to define your search:
|
|
51
51
|
|
|
52
52
|
```yaml
|
|
53
53
|
# yaml-language-server: $schema=./query.schema.json
|
|
54
|
-
name:
|
|
54
|
+
name: "my review"
|
|
55
55
|
description: "Literature search for scoping review"
|
|
56
56
|
|
|
57
57
|
query:
|
|
@@ -70,14 +70,14 @@ filters:
|
|
|
70
70
|
|
|
71
71
|
3. Validate the query:
|
|
72
72
|
```bash
|
|
73
|
-
search-hub query validate
|
|
73
|
+
search-hub query validate my-review
|
|
74
74
|
```
|
|
75
75
|
|
|
76
|
-
This checks structure, validates controlled vocabulary terms (MeSH, ERIC descriptors, Emtree) against external APIs, and suggests corrections for typos.
|
|
76
|
+
This checks structure, validates controlled vocabulary terms (MeSH, ERIC descriptors, Emtree) against external APIs, and suggests corrections for typos. The query name is automatically resolved to `queries/my-review.yaml`.
|
|
77
77
|
|
|
78
78
|
4. Run search:
|
|
79
79
|
```bash
|
|
80
|
-
search-hub search
|
|
80
|
+
search-hub search my-review
|
|
81
81
|
```
|
|
82
82
|
|
|
83
83
|
5. Export results:
|
|
@@ -91,31 +91,33 @@ Developing an effective search query is iterative. Start broad, then refine base
|
|
|
91
91
|
|
|
92
92
|
### Workflow
|
|
93
93
|
|
|
94
|
-
1. **
|
|
94
|
+
1. **Create a query** - Start with a template:
|
|
95
95
|
```bash
|
|
96
|
-
search-hub
|
|
96
|
+
search-hub query init "my review"
|
|
97
|
+
# Creates queries/my-review.yaml
|
|
97
98
|
```
|
|
98
99
|
|
|
99
|
-
2. **
|
|
100
|
+
2. **Check hit counts** - Preview before downloading:
|
|
100
101
|
```bash
|
|
101
|
-
search-hub
|
|
102
|
-
search-hub results <session-v1> -q "title:diabetes year:2023-2025"
|
|
102
|
+
search-hub search my-review --count-only
|
|
103
103
|
```
|
|
104
104
|
|
|
105
|
-
3. **
|
|
105
|
+
3. **Run the search** - When counts look good:
|
|
106
106
|
```bash
|
|
107
|
-
search-hub
|
|
107
|
+
search-hub search my-review
|
|
108
108
|
```
|
|
109
109
|
|
|
110
|
-
4. **
|
|
110
|
+
4. **Review results** - Check titles to assess quality:
|
|
111
111
|
```bash
|
|
112
|
-
|
|
113
|
-
|
|
112
|
+
search-hub results <session-id> --limit 50
|
|
113
|
+
search-hub results <session-id> -q "title:diabetes year:2023-2025"
|
|
114
114
|
```
|
|
115
115
|
|
|
116
|
-
5. **
|
|
116
|
+
5. **Refine and re-run** - Edit the query file, then iterate:
|
|
117
117
|
```bash
|
|
118
|
-
|
|
118
|
+
$EDITOR queries/my-review.yaml
|
|
119
|
+
search-hub search my-review --count-only # Re-check counts
|
|
120
|
+
search-hub search my-review # Execute full search
|
|
119
121
|
```
|
|
120
122
|
|
|
121
123
|
6. **Compare results with diff** - See what changed:
|
|
@@ -128,22 +130,22 @@ Developing an effective search query is iterative. Start broad, then refine base
|
|
|
128
130
|
|
|
129
131
|
- **Use `--count-only` first**: Check hit counts before downloading full results.
|
|
130
132
|
```bash
|
|
131
|
-
search-hub search
|
|
133
|
+
search-hub search my-review --count-only
|
|
132
134
|
```
|
|
133
135
|
|
|
134
136
|
- **Use `--preview`** to see hit counts with sample titles:
|
|
135
137
|
```bash
|
|
136
|
-
search-hub search
|
|
138
|
+
search-hub search my-review --preview
|
|
137
139
|
```
|
|
138
140
|
|
|
139
141
|
- **Use `--dry-run`** to preview translations: See exactly what query each database will receive.
|
|
140
142
|
```bash
|
|
141
|
-
search-hub search
|
|
143
|
+
search-hub search my-review --dry-run
|
|
142
144
|
```
|
|
143
145
|
|
|
144
146
|
- **Compare removed articles carefully**: When narrowing a search, `--show removed` reveals what you're excluding. If important papers are removed, your refinement may be too aggressive.
|
|
145
147
|
|
|
146
|
-
- **
|
|
148
|
+
- **Track iterations**: Use `query assess` and `query log` to record and review your refinement history.
|
|
147
149
|
|
|
148
150
|
## Fulltext Retrieval
|
|
149
151
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/init.ts"],"names":[],"mappings":"AAOA;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,0EAA0E;IAC1E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,sEAAsE;IACtE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,kDAAkD;IAClD,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,4CAA4C;IAC5C,OAAO,EAAE,OAAO,CAAC;IACjB,sCAAsC;IACtC,UAAU,EAAE,MAAM,CAAC;IACnB,qCAAqC;IACrC,WAAW,EAAE,MAAM,CAAC;IACpB,mCAAmC;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,iCAAiC;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,8DAA8D;IAC9D,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,qEAAqE;IACrE,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,oCAAoC;IACpC,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAqGD;;;;;;;;;;GAUG;AACH,wBAAsB,IAAI,CAAC,OAAO,GAAE,WAAgB,GAAG,OAAO,CAAC,UAAU,CAAC,
|
|
1
|
+
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/init.ts"],"names":[],"mappings":"AAOA;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,0EAA0E;IAC1E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,sEAAsE;IACtE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,kDAAkD;IAClD,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,4CAA4C;IAC5C,OAAO,EAAE,OAAO,CAAC;IACjB,sCAAsC;IACtC,UAAU,EAAE,MAAM,CAAC;IACnB,qCAAqC;IACrC,WAAW,EAAE,MAAM,CAAC;IACpB,mCAAmC;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,iCAAiC;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,8DAA8D;IAC9D,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,qEAAqE;IACrE,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,oCAAoC;IACpC,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAqGD;;;;;;;;;;GAUG;AACH,wBAAsB,IAAI,CAAC,OAAO,GAAE,WAAgB,GAAG,OAAO,CAAC,UAAU,CAAC,CAmDzE"}
|
|
@@ -99,6 +99,7 @@ async function init(options = {}) {
|
|
|
99
99
|
} = options;
|
|
100
100
|
const configPath = join(configDir, "config.toml");
|
|
101
101
|
const sessionsDir = join(dataDir, "sessions");
|
|
102
|
+
const queriesDir = join(dataDir, "queries");
|
|
102
103
|
const result = {
|
|
103
104
|
success: false,
|
|
104
105
|
configPath,
|
|
@@ -118,6 +119,7 @@ async function init(options = {}) {
|
|
|
118
119
|
}
|
|
119
120
|
await mkdir(configDir, { recursive: true });
|
|
120
121
|
await mkdir(sessionsDir, { recursive: true });
|
|
122
|
+
await mkdir(queriesDir, { recursive: true });
|
|
121
123
|
const defaultConfig = getDefaultConfig();
|
|
122
124
|
defaultConfig.session.directory = sessionsDir;
|
|
123
125
|
const configContent = generateConfigContent(defaultConfig);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"init.js","sources":["../../../src/cli/commands/init.ts"],"sourcesContent":["import { mkdir, writeFile, access, constants } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport { stringify as stringifyToml } from '@iarna/toml';\nimport { getDefaultConfig } from '../../config/index.js';\nimport { getConfigDir, getDataDir } from '../../config/paths.js';\nimport type { Config } from '../../config/index.js';\n\n/**\n * Options for the init command.\n */\nexport interface InitOptions {\n /** Config directory (defaults to platform-specific via getConfigDir()) */\n configDir?: string;\n /** Data directory (defaults to platform-specific via getDataDir()) */\n dataDir?: string;\n /** Force overwrite if directory already exists */\n force?: boolean;\n}\n\n/**\n * Result of the init command.\n */\nexport interface InitResult {\n /** Whether initialization was successful */\n success: boolean;\n /** Path to the created config file */\n configPath: string;\n /** Path to the sessions directory */\n sessionsDir: string;\n /** Path to the config directory */\n configDir: string;\n /** Path to the data directory */\n dataDir: string;\n /** Whether files already existed (only when success=false) */\n alreadyExists?: boolean;\n /** Whether existing files were overwritten (only when force=true) */\n overwritten?: boolean;\n /** Message describing the result */\n message?: string;\n}\n\n/**\n * Check if a file or directory exists.\n */\nasync function exists(path: string): Promise<boolean> {\n try {\n await access(path, constants.F_OK);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Convert Config to TOML-compatible object.\n * Removes undefined values and converts to the expected format.\n */\nfunction configToToml(config: Config): Record<string, unknown> {\n return {\n session: {\n directory: config.session.directory,\n },\n log: {\n level: config.log.level,\n },\n output: {\n color: config.output.color,\n progress_bar: config.output.progress_bar,\n },\n providers: {\n pubmed: {\n enabled: config.providers.pubmed.enabled,\n api_key: config.providers.pubmed.api_key ?? '',\n email: config.providers.pubmed.email ?? '',\n rate_limit: config.providers.pubmed.rate_limit,\n timeout: config.providers.pubmed.timeout,\n retries: config.providers.pubmed.retries,\n max_results: config.providers.pubmed.max_results,\n },\n eric: {\n enabled: config.providers.eric.enabled,\n rate_limit: config.providers.eric.rate_limit,\n timeout: config.providers.eric.timeout,\n retries: config.providers.eric.retries,\n max_results: config.providers.eric.max_results,\n },\n arxiv: {\n enabled: config.providers.arxiv.enabled,\n rate_limit: config.providers.arxiv.rate_limit,\n timeout: config.providers.arxiv.timeout,\n retries: config.providers.arxiv.retries,\n max_results: config.providers.arxiv.max_results,\n },\n scopus: {\n enabled: config.providers.scopus.enabled,\n api_key: config.providers.scopus.api_key ?? '',\n inst_token: config.providers.scopus.inst_token ?? '',\n rate_limit: config.providers.scopus.rate_limit,\n timeout: config.providers.scopus.timeout,\n retries: config.providers.scopus.retries,\n max_results: config.providers.scopus.max_results,\n },\n wos: {\n enabled: config.providers.wos.enabled,\n api_key: config.providers.wos.api_key ?? '',\n rate_limit: config.providers.wos.rate_limit,\n timeout: config.providers.wos.timeout,\n retries: config.providers.wos.retries,\n max_results: config.providers.wos.max_results,\n },\n embase: {\n enabled: config.providers.embase.enabled,\n rate_limit: config.providers.embase.rate_limit,\n timeout: config.providers.embase.timeout,\n retries: config.providers.embase.retries,\n max_results: config.providers.embase.max_results,\n },\n },\n integration: {\n reference_manager: {\n enabled: config.integration.reference_manager.enabled,\n command: config.integration.reference_manager.command,\n auto_register: config.integration.reference_manager.auto_register,\n },\n },\n };\n}\n\n/**\n * Generate TOML config file content with comments.\n */\nfunction generateConfigContent(config: Config): string {\n const tomlObj = configToToml(config);\n const header = `# search-hub configuration file\n# See: https://github.com/search-hub/search-hub for documentation\n\n`;\n return header + stringifyToml(tomlObj as Parameters<typeof stringifyToml>[0]);\n}\n\n/**\n * Initialize the search-hub configuration directory.\n *\n * Creates:\n * - Config directory with config.toml\n * - Data directory with sessions/ subdirectory\n *\n * On Linux (XDG):\n * - ~/.config/search-hub/config.toml\n * - ~/.local/share/search-hub/sessions/\n */\nexport async function init(options: InitOptions = {}): Promise<InitResult> {\n const {\n configDir = getConfigDir(),\n dataDir = getDataDir(),\n force = false,\n } = options;\n\n const configPath = join(configDir, 'config.toml');\n const sessionsDir = join(dataDir, 'sessions');\n\n const result: InitResult = {\n success: false,\n configPath,\n sessionsDir,\n configDir,\n dataDir,\n };\n\n // Check if config directory already exists\n if (await exists(configDir)) {\n if (!force) {\n return {\n ...result,\n alreadyExists: true,\n message: `Configuration directory already exists at ${configDir}. Use --force to overwrite.`,\n };\n }\n result.overwritten = true;\n }\n\n // Create directories\n await mkdir(configDir, { recursive: true });\n await mkdir(sessionsDir, { recursive: true });\n\n // Generate and write config file\n // Use the default sessions directory for the saved config\n const defaultConfig = getDefaultConfig();\n // Set session.directory to the actual sessions path for the config file\n defaultConfig.session.directory = sessionsDir;\n const configContent = generateConfigContent(defaultConfig);\n await writeFile(configPath, configContent, 'utf-8');\n\n return {\n ...result,\n success: true,\n message: result.overwritten\n ? `Configuration overwritten at ${configDir}`\n : `Configuration created at ${configDir}`,\n };\n}\n"],"names":["stringifyToml"],"mappings":"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
|
|
1
|
+
{"version":3,"file":"init.js","sources":["../../../src/cli/commands/init.ts"],"sourcesContent":["import { mkdir, writeFile, access, constants } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport { stringify as stringifyToml } from '@iarna/toml';\nimport { getDefaultConfig } from '../../config/index.js';\nimport { getConfigDir, getDataDir } from '../../config/paths.js';\nimport type { Config } from '../../config/index.js';\n\n/**\n * Options for the init command.\n */\nexport interface InitOptions {\n /** Config directory (defaults to platform-specific via getConfigDir()) */\n configDir?: string;\n /** Data directory (defaults to platform-specific via getDataDir()) */\n dataDir?: string;\n /** Force overwrite if directory already exists */\n force?: boolean;\n}\n\n/**\n * Result of the init command.\n */\nexport interface InitResult {\n /** Whether initialization was successful */\n success: boolean;\n /** Path to the created config file */\n configPath: string;\n /** Path to the sessions directory */\n sessionsDir: string;\n /** Path to the config directory */\n configDir: string;\n /** Path to the data directory */\n dataDir: string;\n /** Whether files already existed (only when success=false) */\n alreadyExists?: boolean;\n /** Whether existing files were overwritten (only when force=true) */\n overwritten?: boolean;\n /** Message describing the result */\n message?: string;\n}\n\n/**\n * Check if a file or directory exists.\n */\nasync function exists(path: string): Promise<boolean> {\n try {\n await access(path, constants.F_OK);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Convert Config to TOML-compatible object.\n * Removes undefined values and converts to the expected format.\n */\nfunction configToToml(config: Config): Record<string, unknown> {\n return {\n session: {\n directory: config.session.directory,\n },\n log: {\n level: config.log.level,\n },\n output: {\n color: config.output.color,\n progress_bar: config.output.progress_bar,\n },\n providers: {\n pubmed: {\n enabled: config.providers.pubmed.enabled,\n api_key: config.providers.pubmed.api_key ?? '',\n email: config.providers.pubmed.email ?? '',\n rate_limit: config.providers.pubmed.rate_limit,\n timeout: config.providers.pubmed.timeout,\n retries: config.providers.pubmed.retries,\n max_results: config.providers.pubmed.max_results,\n },\n eric: {\n enabled: config.providers.eric.enabled,\n rate_limit: config.providers.eric.rate_limit,\n timeout: config.providers.eric.timeout,\n retries: config.providers.eric.retries,\n max_results: config.providers.eric.max_results,\n },\n arxiv: {\n enabled: config.providers.arxiv.enabled,\n rate_limit: config.providers.arxiv.rate_limit,\n timeout: config.providers.arxiv.timeout,\n retries: config.providers.arxiv.retries,\n max_results: config.providers.arxiv.max_results,\n },\n scopus: {\n enabled: config.providers.scopus.enabled,\n api_key: config.providers.scopus.api_key ?? '',\n inst_token: config.providers.scopus.inst_token ?? '',\n rate_limit: config.providers.scopus.rate_limit,\n timeout: config.providers.scopus.timeout,\n retries: config.providers.scopus.retries,\n max_results: config.providers.scopus.max_results,\n },\n wos: {\n enabled: config.providers.wos.enabled,\n api_key: config.providers.wos.api_key ?? '',\n rate_limit: config.providers.wos.rate_limit,\n timeout: config.providers.wos.timeout,\n retries: config.providers.wos.retries,\n max_results: config.providers.wos.max_results,\n },\n embase: {\n enabled: config.providers.embase.enabled,\n rate_limit: config.providers.embase.rate_limit,\n timeout: config.providers.embase.timeout,\n retries: config.providers.embase.retries,\n max_results: config.providers.embase.max_results,\n },\n },\n integration: {\n reference_manager: {\n enabled: config.integration.reference_manager.enabled,\n command: config.integration.reference_manager.command,\n auto_register: config.integration.reference_manager.auto_register,\n },\n },\n };\n}\n\n/**\n * Generate TOML config file content with comments.\n */\nfunction generateConfigContent(config: Config): string {\n const tomlObj = configToToml(config);\n const header = `# search-hub configuration file\n# See: https://github.com/search-hub/search-hub for documentation\n\n`;\n return header + stringifyToml(tomlObj as Parameters<typeof stringifyToml>[0]);\n}\n\n/**\n * Initialize the search-hub configuration directory.\n *\n * Creates:\n * - Config directory with config.toml\n * - Data directory with sessions/ subdirectory\n *\n * On Linux (XDG):\n * - ~/.config/search-hub/config.toml\n * - ~/.local/share/search-hub/sessions/\n */\nexport async function init(options: InitOptions = {}): Promise<InitResult> {\n const {\n configDir = getConfigDir(),\n dataDir = getDataDir(),\n force = false,\n } = options;\n\n const configPath = join(configDir, 'config.toml');\n const sessionsDir = join(dataDir, 'sessions');\n const queriesDir = join(dataDir, 'queries');\n\n const result: InitResult = {\n success: false,\n configPath,\n sessionsDir,\n configDir,\n dataDir,\n };\n\n // Check if config directory already exists\n if (await exists(configDir)) {\n if (!force) {\n return {\n ...result,\n alreadyExists: true,\n message: `Configuration directory already exists at ${configDir}. Use --force to overwrite.`,\n };\n }\n result.overwritten = true;\n }\n\n // Create directories\n await mkdir(configDir, { recursive: true });\n await mkdir(sessionsDir, { recursive: true });\n await mkdir(queriesDir, { recursive: true });\n\n // Generate and write config file\n // Use the default sessions directory for the saved config\n const defaultConfig = getDefaultConfig();\n // Set session.directory to the actual sessions path for the config file\n defaultConfig.session.directory = sessionsDir;\n const configContent = generateConfigContent(defaultConfig);\n await writeFile(configPath, configContent, 'utf-8');\n\n return {\n ...result,\n success: true,\n message: result.overwritten\n ? `Configuration overwritten at ${configDir}`\n : `Configuration created at ${configDir}`,\n };\n}\n"],"names":["stringifyToml"],"mappings":"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"}
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sanitize a title string into a safe filename (without extension).
|
|
3
|
+
*/
|
|
4
|
+
export declare function sanitizeForFilename(title: string): string;
|
|
1
5
|
/**
|
|
2
6
|
* The YAML template string with comments preserved.
|
|
3
7
|
* This is a raw string (not generated by a YAML library) so comments are kept.
|
|
@@ -6,22 +10,29 @@ export declare const QUERY_SCHEMA_FILENAME = "query.schema.json";
|
|
|
6
10
|
/**
|
|
7
11
|
* Generate the query template YAML string.
|
|
8
12
|
*
|
|
13
|
+
* @param title - Optional title to set as the query name
|
|
9
14
|
* @returns The YAML template string with comments
|
|
10
15
|
*/
|
|
11
|
-
export declare function generateQueryTemplate(): string;
|
|
16
|
+
export declare function generateQueryTemplate(title?: string): string;
|
|
17
|
+
export declare const QUERIES_DIR = "queries";
|
|
18
|
+
export interface WriteQueryTemplateOptions {
|
|
19
|
+
title: string;
|
|
20
|
+
output?: string | undefined;
|
|
21
|
+
stdout?: boolean | undefined;
|
|
22
|
+
force?: boolean | undefined;
|
|
23
|
+
cwd?: string | undefined;
|
|
24
|
+
}
|
|
12
25
|
/**
|
|
13
26
|
* Write the query template to a file or return it as a message.
|
|
14
27
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
28
|
+
* Output priority:
|
|
29
|
+
* 1. --stdout → return template as message (no file)
|
|
30
|
+
* 2. -o <path> → write to that path
|
|
31
|
+
* 3. default → write to queries/<sanitized-title>.yaml
|
|
19
32
|
*/
|
|
20
|
-
export declare function writeQueryTemplate(options: {
|
|
21
|
-
output?: string;
|
|
22
|
-
force?: boolean;
|
|
23
|
-
}): Promise<{
|
|
33
|
+
export declare function writeQueryTemplate(options: WriteQueryTemplateOptions): Promise<{
|
|
24
34
|
success: boolean;
|
|
25
35
|
message: string;
|
|
36
|
+
outputPath?: string;
|
|
26
37
|
}>;
|
|
27
38
|
//# sourceMappingURL=init.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/query/init.ts"],"names":[],"mappings":"AASA;;;GAGG;AACH,eAAO,MAAM,qBAAqB,sBAAsB,CAAC;AA2DzD
|
|
1
|
+
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/query/init.ts"],"names":[],"mappings":"AASA;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAYzD;AAED;;;GAGG;AACH,eAAO,MAAM,qBAAqB,sBAAsB,CAAC;AA2DzD;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAI5D;AAED,eAAO,MAAM,WAAW,YAAY,CAAC;AAErC,MAAM,WAAW,yBAAyB;IACxC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,MAAM,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC7B,KAAK,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC5B,GAAG,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC1B;AAED;;;;;;;GAOG;AACH,wBAAsB,kBAAkB,CAAC,OAAO,EAAE,yBAAyB,GAAG,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAwChJ"}
|
|
@@ -1,6 +1,13 @@
|
|
|
1
|
-
import { access, writeFile } from "node:fs/promises";
|
|
1
|
+
import { access, mkdir, writeFile } from "node:fs/promises";
|
|
2
2
|
import { join, dirname } from "node:path";
|
|
3
3
|
import { generateQueryJSONSchema } from "../../../query/json-schema.js";
|
|
4
|
+
function sanitizeForFilename(title) {
|
|
5
|
+
const result = title.trim().toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9_-]/g, "").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
6
|
+
if (!result) {
|
|
7
|
+
throw new Error("Title produces an empty filename after sanitization");
|
|
8
|
+
}
|
|
9
|
+
return result;
|
|
10
|
+
}
|
|
4
11
|
const QUERY_SCHEMA_FILENAME = "query.schema.json";
|
|
5
12
|
const QUERY_TEMPLATE = `# yaml-language-server: $schema=./${QUERY_SCHEMA_FILENAME}
|
|
6
13
|
name: my_search
|
|
@@ -57,36 +64,44 @@ query:
|
|
|
57
64
|
# exclude:
|
|
58
65
|
# - "Letter"
|
|
59
66
|
`;
|
|
60
|
-
function generateQueryTemplate() {
|
|
61
|
-
return QUERY_TEMPLATE;
|
|
67
|
+
function generateQueryTemplate(title) {
|
|
68
|
+
if (!title) return QUERY_TEMPLATE;
|
|
69
|
+
const escaped = title.replace(/"/g, '\\"');
|
|
70
|
+
return QUERY_TEMPLATE.replace("name: my_search", `name: "${escaped}"`);
|
|
62
71
|
}
|
|
72
|
+
const QUERIES_DIR = "queries";
|
|
63
73
|
async function writeQueryTemplate(options) {
|
|
64
|
-
const template = generateQueryTemplate();
|
|
65
|
-
if (
|
|
74
|
+
const template = generateQueryTemplate(options.title);
|
|
75
|
+
if (options.stdout) {
|
|
66
76
|
return { success: true, message: template };
|
|
67
77
|
}
|
|
78
|
+
const outputPath = options.output ?? join(options.cwd ?? process.cwd(), QUERIES_DIR, `${sanitizeForFilename(options.title)}.yaml`);
|
|
68
79
|
if (!options.force) {
|
|
69
80
|
try {
|
|
70
|
-
await access(
|
|
81
|
+
await access(outputPath);
|
|
71
82
|
return {
|
|
72
83
|
success: false,
|
|
73
|
-
message: `File already exists: ${
|
|
84
|
+
message: `File already exists: ${outputPath}. Use --force to overwrite.`
|
|
74
85
|
};
|
|
75
86
|
} catch {
|
|
76
87
|
}
|
|
77
88
|
}
|
|
78
|
-
await
|
|
79
|
-
|
|
89
|
+
await mkdir(dirname(outputPath), { recursive: true });
|
|
90
|
+
await writeFile(outputPath, template, "utf-8");
|
|
91
|
+
const schemaPath = join(dirname(outputPath), QUERY_SCHEMA_FILENAME);
|
|
80
92
|
const jsonSchema = generateQueryJSONSchema();
|
|
81
93
|
await writeFile(schemaPath, JSON.stringify(jsonSchema, null, 2) + "\n", "utf-8");
|
|
82
94
|
return {
|
|
83
95
|
success: true,
|
|
84
|
-
message: `
|
|
96
|
+
message: `Created: ${outputPath}`,
|
|
97
|
+
outputPath
|
|
85
98
|
};
|
|
86
99
|
}
|
|
87
100
|
export {
|
|
101
|
+
QUERIES_DIR,
|
|
88
102
|
QUERY_SCHEMA_FILENAME,
|
|
89
103
|
generateQueryTemplate,
|
|
104
|
+
sanitizeForFilename,
|
|
90
105
|
writeQueryTemplate
|
|
91
106
|
};
|
|
92
107
|
//# sourceMappingURL=init.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"init.js","sources":["../../../../src/cli/commands/query/init.ts"],"sourcesContent":["/**\n * Query init command implementation.\n *\n * Generates a skeleton YAML query file with helpful comments.\n */\nimport { writeFile as fsWriteFile, access } from \"node:fs/promises\";\nimport { dirname, join } from \"node:path\";\nimport { generateQueryJSONSchema } from \"../../../query/json-schema.js\";\n\n/**\n * The YAML template string with comments preserved.\n * This is a raw string (not generated by a YAML library) so comments are kept.\n */\nexport const QUERY_SCHEMA_FILENAME = \"query.schema.json\";\n\n// prettier-ignore\nconst QUERY_TEMPLATE =\n `# yaml-language-server: $schema=./${QUERY_SCHEMA_FILENAME}\\n` +\n \"name: my_search\\n\" +\n \"description: \\\"\\\"\\n\" +\n \"\\n\" +\n \"query:\\n\" +\n \" - id: concept-1 # Unique block identifier (for provider replacements)\\n\" +\n \" field: title_abstract # title, abstract, title_abstract, author, keyword, all\\n\" +\n \" terms:\\n\" +\n \" keywords:\\n\" +\n \" - \\\"search term 1\\\"\\n\" +\n \" - \\\"search term 2\\\"\\n\" +\n \" # mesh: # PubMed MeSH terms (optional)\\n\" +\n \" # - \\\"MeSH Heading\\\"\\n\" +\n \" # eric: # ERIC Descriptors (optional, ERIC only)\\n\" +\n \" # - \\\"ERIC Descriptor\\\"\\n\" +\n \" exclude: [] # Terms to exclude (NOT operator)\\n\" +\n \" # Tip: Use exclude to filter out false matches from short keywords/acronyms\\n\" +\n \" # exclude:\\n\" +\n \" # - \\\"unwanted term\\\"\\n\" +\n \" # - \\\"irrelevant topic\\\"\\n\" +\n \" operator: OR # How to combine terms within this block\\n\" +\n \"\\n\" +\n \" # Add more blocks — blocks are AND'd together\\n\" +\n \" # - id: concept-2\\n\" +\n \" # field: title_abstract\\n\" +\n \" # terms:\\n\" +\n \" # keywords:\\n\" +\n \" # - \\\"another term\\\"\\n\" +\n \" # operator: OR\\n\" +\n \"\\n\" +\n \"# filters: # Optional: apply to all databases\\n\" +\n \"# year_from: 2020\\n\" +\n \"# year_to: 2026\\n\" +\n \"# language:\\n\" +\n \"# - en\\n\" +\n \"# publication_types:\\n\" +\n \"# exclude:\\n\" +\n \"# - \\\"Review\\\"\\n\" +\n \"# - \\\"Comment\\\"\\n\" +\n \"\\n\" +\n \"# providers: # Optional: per-database block replacements & filter additions\\n\" +\n \"# pubmed:\\n\" +\n \"# replaces:\\n\" +\n \"# concept-1: # Replace block by id\\n\" +\n \"# field: keyword\\n\" +\n \"# terms:\\n\" +\n \"# mesh:\\n\" +\n \"# - \\\"MeSH Heading\\\"\\n\" +\n \"# operator: OR\\n\" +\n \"# adds:\\n\" +\n \"# filters:\\n\" +\n \"# publication_types:\\n\" +\n \"# exclude:\\n\" +\n \"# - \\\"Letter\\\"\\n\";\n\n/**\n * Generate the query template YAML string.\n *\n * @returns The YAML template string with comments\n */\nexport function generateQueryTemplate(): string {\n return QUERY_TEMPLATE;\n}\n\n/**\n * Write the query template to a file or return it as a message.\n *\n *
|
|
1
|
+
{"version":3,"file":"init.js","sources":["../../../../src/cli/commands/query/init.ts"],"sourcesContent":["/**\n * Query init command implementation.\n *\n * Generates a skeleton YAML query file with helpful comments.\n */\nimport { writeFile as fsWriteFile, access, mkdir } from \"node:fs/promises\";\nimport { dirname, join } from \"node:path\";\nimport { generateQueryJSONSchema } from \"../../../query/json-schema.js\";\n\n/**\n * Sanitize a title string into a safe filename (without extension).\n */\nexport function sanitizeForFilename(title: string): string {\n const result = title\n .trim()\n .toLowerCase()\n .replace(/\\s+/g, '-')\n .replace(/[^a-z0-9_-]/g, '')\n .replace(/-+/g, '-')\n .replace(/^-|-$/g, '');\n if (!result) {\n throw new Error('Title produces an empty filename after sanitization');\n }\n return result;\n}\n\n/**\n * The YAML template string with comments preserved.\n * This is a raw string (not generated by a YAML library) so comments are kept.\n */\nexport const QUERY_SCHEMA_FILENAME = \"query.schema.json\";\n\n// prettier-ignore\nconst QUERY_TEMPLATE =\n `# yaml-language-server: $schema=./${QUERY_SCHEMA_FILENAME}\\n` +\n \"name: my_search\\n\" +\n \"description: \\\"\\\"\\n\" +\n \"\\n\" +\n \"query:\\n\" +\n \" - id: concept-1 # Unique block identifier (for provider replacements)\\n\" +\n \" field: title_abstract # title, abstract, title_abstract, author, keyword, all\\n\" +\n \" terms:\\n\" +\n \" keywords:\\n\" +\n \" - \\\"search term 1\\\"\\n\" +\n \" - \\\"search term 2\\\"\\n\" +\n \" # mesh: # PubMed MeSH terms (optional)\\n\" +\n \" # - \\\"MeSH Heading\\\"\\n\" +\n \" # eric: # ERIC Descriptors (optional, ERIC only)\\n\" +\n \" # - \\\"ERIC Descriptor\\\"\\n\" +\n \" exclude: [] # Terms to exclude (NOT operator)\\n\" +\n \" # Tip: Use exclude to filter out false matches from short keywords/acronyms\\n\" +\n \" # exclude:\\n\" +\n \" # - \\\"unwanted term\\\"\\n\" +\n \" # - \\\"irrelevant topic\\\"\\n\" +\n \" operator: OR # How to combine terms within this block\\n\" +\n \"\\n\" +\n \" # Add more blocks — blocks are AND'd together\\n\" +\n \" # - id: concept-2\\n\" +\n \" # field: title_abstract\\n\" +\n \" # terms:\\n\" +\n \" # keywords:\\n\" +\n \" # - \\\"another term\\\"\\n\" +\n \" # operator: OR\\n\" +\n \"\\n\" +\n \"# filters: # Optional: apply to all databases\\n\" +\n \"# year_from: 2020\\n\" +\n \"# year_to: 2026\\n\" +\n \"# language:\\n\" +\n \"# - en\\n\" +\n \"# publication_types:\\n\" +\n \"# exclude:\\n\" +\n \"# - \\\"Review\\\"\\n\" +\n \"# - \\\"Comment\\\"\\n\" +\n \"\\n\" +\n \"# providers: # Optional: per-database block replacements & filter additions\\n\" +\n \"# pubmed:\\n\" +\n \"# replaces:\\n\" +\n \"# concept-1: # Replace block by id\\n\" +\n \"# field: keyword\\n\" +\n \"# terms:\\n\" +\n \"# mesh:\\n\" +\n \"# - \\\"MeSH Heading\\\"\\n\" +\n \"# operator: OR\\n\" +\n \"# adds:\\n\" +\n \"# filters:\\n\" +\n \"# publication_types:\\n\" +\n \"# exclude:\\n\" +\n \"# - \\\"Letter\\\"\\n\";\n\n/**\n * Generate the query template YAML string.\n *\n * @param title - Optional title to set as the query name\n * @returns The YAML template string with comments\n */\nexport function generateQueryTemplate(title?: string): string {\n if (!title) return QUERY_TEMPLATE;\n const escaped = title.replace(/\"/g, '\\\\\"');\n return QUERY_TEMPLATE.replace('name: my_search', `name: \"${escaped}\"`);\n}\n\nexport const QUERIES_DIR = \"queries\";\n\nexport interface WriteQueryTemplateOptions {\n title: string;\n output?: string | undefined;\n stdout?: boolean | undefined;\n force?: boolean | undefined;\n cwd?: string | undefined;\n}\n\n/**\n * Write the query template to a file or return it as a message.\n *\n * Output priority:\n * 1. --stdout → return template as message (no file)\n * 2. -o <path> → write to that path\n * 3. default → write to queries/<sanitized-title>.yaml\n */\nexport async function writeQueryTemplate(options: WriteQueryTemplateOptions): Promise<{ success: boolean; message: string; outputPath?: string }> {\n const template = generateQueryTemplate(options.title);\n\n if (options.stdout) {\n return { success: true, message: template };\n }\n\n // Determine output path\n const outputPath = options.output\n ?? join(options.cwd ?? process.cwd(), QUERIES_DIR, `${sanitizeForFilename(options.title)}.yaml`);\n\n // Check if file exists (unless force is set)\n if (!options.force) {\n try {\n await access(outputPath);\n return {\n success: false,\n message: `File already exists: ${outputPath}. Use --force to overwrite.`,\n };\n } catch {\n // File does not exist, proceed\n }\n }\n\n // Ensure parent directory exists\n await mkdir(dirname(outputPath), { recursive: true });\n\n // Write template\n await fsWriteFile(outputPath, template, \"utf-8\");\n\n // Generate JSON Schema file alongside output\n const schemaPath = join(dirname(outputPath), QUERY_SCHEMA_FILENAME);\n const jsonSchema = generateQueryJSONSchema();\n await fsWriteFile(schemaPath, JSON.stringify(jsonSchema, null, 2) + \"\\n\", \"utf-8\");\n\n return {\n success: true,\n message: `Created: ${outputPath}`,\n outputPath,\n };\n}\n"],"names":["fsWriteFile"],"mappings":";;;AAYO,SAAS,oBAAoB,OAAuB;AACzD,QAAM,SAAS,MACZ,KAAA,EACA,cACA,QAAQ,QAAQ,GAAG,EACnB,QAAQ,gBAAgB,EAAE,EAC1B,QAAQ,OAAO,GAAG,EAClB,QAAQ,UAAU,EAAE;AACvB,MAAI,CAAC,QAAQ;AACX,UAAM,IAAI,MAAM,qDAAqD;AAAA,EACvE;AACA,SAAO;AACT;AAMO,MAAM,wBAAwB;AAGrC,MAAM,iBACJ,qCAAqC,qBAAqB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA6DrD,SAAS,sBAAsB,OAAwB;AAC5D,MAAI,CAAC,MAAO,QAAO;AACnB,QAAM,UAAU,MAAM,QAAQ,MAAM,KAAK;AACzC,SAAO,eAAe,QAAQ,mBAAmB,UAAU,OAAO,GAAG;AACvE;AAEO,MAAM,cAAc;AAkB3B,eAAsB,mBAAmB,SAAyG;AAChJ,QAAM,WAAW,sBAAsB,QAAQ,KAAK;AAEpD,MAAI,QAAQ,QAAQ;AAClB,WAAO,EAAE,SAAS,MAAM,SAAS,SAAA;AAAA,EACnC;AAGA,QAAM,aAAa,QAAQ,UACtB,KAAK,QAAQ,OAAO,QAAQ,IAAA,GAAO,aAAa,GAAG,oBAAoB,QAAQ,KAAK,CAAC,OAAO;AAGjG,MAAI,CAAC,QAAQ,OAAO;AAClB,QAAI;AACF,YAAM,OAAO,UAAU;AACvB,aAAO;AAAA,QACL,SAAS;AAAA,QACT,SAAS,wBAAwB,UAAU;AAAA,MAAA;AAAA,IAE/C,QAAQ;AAAA,IAER;AAAA,EACF;AAGA,QAAM,MAAM,QAAQ,UAAU,GAAG,EAAE,WAAW,MAAM;AAGpD,QAAMA,UAAY,YAAY,UAAU,OAAO;AAG/C,QAAM,aAAa,KAAK,QAAQ,UAAU,GAAG,qBAAqB;AAClE,QAAM,aAAa,wBAAA;AACnB,QAAMA,UAAY,YAAY,KAAK,UAAU,YAAY,MAAM,CAAC,IAAI,MAAM,OAAO;AAEjF,SAAO;AAAA,IACL,SAAS;AAAA,IACT,SAAS,YAAY,UAAU;AAAA,IAC/B;AAAA,EAAA;AAEJ;"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolve.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/query/resolve.ts"],"names":[],"mappings":"AAYA,qBAAa,aAAc,SAAQ,KAAK;gBAC1B,IAAI,EAAE,MAAM;CAIzB;AAiBD,wBAAsB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CA0CnE"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { stat } from "node:fs/promises";
|
|
2
|
+
class NotAFileError extends Error {
|
|
3
|
+
constructor(path) {
|
|
4
|
+
super(`Path is not a file: ${path}`);
|
|
5
|
+
this.name = "NotAFileError";
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
async function isFile(path) {
|
|
9
|
+
try {
|
|
10
|
+
const s = await stat(path);
|
|
11
|
+
if (!s.isFile()) {
|
|
12
|
+
throw new NotAFileError(path);
|
|
13
|
+
}
|
|
14
|
+
return true;
|
|
15
|
+
} catch (error) {
|
|
16
|
+
if (error instanceof NotAFileError) {
|
|
17
|
+
throw error;
|
|
18
|
+
}
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
async function resolveQueryFile(arg) {
|
|
23
|
+
if (await isFile(arg)) {
|
|
24
|
+
return arg;
|
|
25
|
+
}
|
|
26
|
+
const candidates = [];
|
|
27
|
+
if (!arg.endsWith(".yaml") && !arg.endsWith(".yml")) {
|
|
28
|
+
const withExt = `${arg}.yaml`;
|
|
29
|
+
candidates.push(withExt);
|
|
30
|
+
if (await isFile(withExt)) {
|
|
31
|
+
return withExt;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const basename = arg.endsWith(".yaml") || arg.endsWith(".yml") ? arg : `${arg}.yaml`;
|
|
35
|
+
const inQueries = `queries/${basename}`;
|
|
36
|
+
candidates.push(inQueries);
|
|
37
|
+
if (await isFile(inQueries)) {
|
|
38
|
+
return inQueries;
|
|
39
|
+
}
|
|
40
|
+
if (!arg.endsWith(".yaml") && !arg.endsWith(".yml")) {
|
|
41
|
+
const inQueriesYml = `queries/${arg}.yml`;
|
|
42
|
+
candidates.push(inQueriesYml);
|
|
43
|
+
if (await isFile(inQueriesYml)) {
|
|
44
|
+
return inQueriesYml;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const tried = [`./${arg}`, ...candidates.map((c) => `./${c}`)];
|
|
48
|
+
throw new Error(
|
|
49
|
+
`Query file not found: "${arg}"
|
|
50
|
+
Tried:
|
|
51
|
+
` + tried.map((p) => ` ${p}`).join("\n") + `
|
|
52
|
+
Create a new query: search-hub query init "${arg}"`
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
export {
|
|
56
|
+
NotAFileError,
|
|
57
|
+
resolveQueryFile
|
|
58
|
+
};
|
|
59
|
+
//# sourceMappingURL=resolve.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolve.js","sources":["../../../../src/cli/commands/query/resolve.ts"],"sourcesContent":["/**\n * Smart query file resolution.\n *\n * Resolution order:\n * 1. Exact path exists → use it\n * 2. <arg>.yaml exists → use it\n * 3. queries/<arg>.yaml exists → use it\n * 4. queries/<arg>.yml exists → use it\n * 5. Error with tried paths\n */\nimport { stat } from 'node:fs/promises';\n\nexport class NotAFileError extends Error {\n constructor(path: string) {\n super(`Path is not a file: ${path}`);\n this.name = 'NotAFileError';\n }\n}\n\nasync function isFile(path: string): Promise<boolean> {\n try {\n const s = await stat(path);\n if (!s.isFile()) {\n throw new NotAFileError(path);\n }\n return true;\n } catch (error) {\n if (error instanceof NotAFileError) {\n throw error;\n }\n return false;\n }\n}\n\nexport async function resolveQueryFile(arg: string): Promise<string> {\n // 1. Exact path\n if (await isFile(arg)) {\n return arg;\n }\n\n const candidates: string[] = [];\n\n // 2. arg + .yaml (skip if already ends with .yaml)\n if (!arg.endsWith('.yaml') && !arg.endsWith('.yml')) {\n const withExt = `${arg}.yaml`;\n candidates.push(withExt);\n if (await isFile(withExt)) {\n return withExt;\n }\n }\n\n // 3. queries/<arg>.yaml\n const basename = arg.endsWith('.yaml') || arg.endsWith('.yml') ? arg : `${arg}.yaml`;\n const inQueries = `queries/${basename}`;\n candidates.push(inQueries);\n if (await isFile(inQueries)) {\n return inQueries;\n }\n\n // 4. queries/<arg>.yml (skip if arg already has extension)\n if (!arg.endsWith('.yaml') && !arg.endsWith('.yml')) {\n const inQueriesYml = `queries/${arg}.yml`;\n candidates.push(inQueriesYml);\n if (await isFile(inQueriesYml)) {\n return inQueriesYml;\n }\n }\n\n // 5. Error\n const tried = [`./${arg}`, ...candidates.map(c => `./${c}`)];\n throw new Error(\n `Query file not found: \"${arg}\"\\n` +\n ` Tried:\\n` +\n tried.map(p => ` ${p}`).join('\\n') + '\\n' +\n ` Create a new query: search-hub query init \"${arg}\"`\n );\n}\n"],"names":[],"mappings":";AAYO,MAAM,sBAAsB,MAAM;AAAA,EACvC,YAAY,MAAc;AACxB,UAAM,uBAAuB,IAAI,EAAE;AACnC,SAAK,OAAO;AAAA,EACd;AACF;AAEA,eAAe,OAAO,MAAgC;AACpD,MAAI;AACF,UAAM,IAAI,MAAM,KAAK,IAAI;AACzB,QAAI,CAAC,EAAE,UAAU;AACf,YAAM,IAAI,cAAc,IAAI;AAAA,IAC9B;AACA,WAAO;AAAA,EACT,SAAS,OAAO;AACd,QAAI,iBAAiB,eAAe;AAClC,YAAM;AAAA,IACR;AACA,WAAO;AAAA,EACT;AACF;AAEA,eAAsB,iBAAiB,KAA8B;AAEnE,MAAI,MAAM,OAAO,GAAG,GAAG;AACrB,WAAO;AAAA,EACT;AAEA,QAAM,aAAuB,CAAA;AAG7B,MAAI,CAAC,IAAI,SAAS,OAAO,KAAK,CAAC,IAAI,SAAS,MAAM,GAAG;AACnD,UAAM,UAAU,GAAG,GAAG;AACtB,eAAW,KAAK,OAAO;AACvB,QAAI,MAAM,OAAO,OAAO,GAAG;AACzB,aAAO;AAAA,IACT;AAAA,EACF;AAGA,QAAM,WAAW,IAAI,SAAS,OAAO,KAAK,IAAI,SAAS,MAAM,IAAI,MAAM,GAAG,GAAG;AAC7E,QAAM,YAAY,WAAW,QAAQ;AACrC,aAAW,KAAK,SAAS;AACzB,MAAI,MAAM,OAAO,SAAS,GAAG;AAC3B,WAAO;AAAA,EACT;AAGA,MAAI,CAAC,IAAI,SAAS,OAAO,KAAK,CAAC,IAAI,SAAS,MAAM,GAAG;AACnD,UAAM,eAAe,WAAW,GAAG;AACnC,eAAW,KAAK,YAAY;AAC5B,QAAI,MAAM,OAAO,YAAY,GAAG;AAC9B,aAAO;AAAA,IACT;AAAA,EACF;AAGA,QAAM,QAAQ,CAAC,KAAK,GAAG,IAAI,GAAG,WAAW,IAAI,CAAA,MAAK,KAAK,CAAC,EAAE,CAAC;AAC3D,QAAM,IAAI;AAAA,IACR,0BAA0B,GAAG;AAAA;AAAA,IAE7B,MAAM,IAAI,CAAA,MAAK,OAAO,CAAC,EAAE,EAAE,KAAK,IAAI,IAAI;AAAA,+CACQ,GAAG;AAAA,EAAA;AAEvD;"}
|
package/dist/cli/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":";AAQA,OAAO,EAAE,OAAO,EAAU,MAAM,WAAW,CAAC;AAyM5C;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,0BAA0B;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gCAAgC;IAChC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,4BAA4B;IAC5B,OAAO,EAAE,OAAO,CAAC;IACjB,wCAAwC;IACxC,KAAK,EAAE,OAAO,CAAC;IACf,qEAAqE;IACrE,KAAK,EAAE,OAAO,CAAC;CAChB;AAED;;GAEG;AACH,wBAAgB,aAAa,IAAI,OAAO,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":";AAQA,OAAO,EAAE,OAAO,EAAU,MAAM,WAAW,CAAC;AAyM5C;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,0BAA0B;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gCAAgC;IAChC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,4BAA4B;IAC5B,OAAO,EAAE,OAAO,CAAC;IACjB,wCAAwC;IACxC,KAAK,EAAE,OAAO,CAAC;IACf,qEAAqE;IACrE,KAAK,EAAE,OAAO,CAAC;CAChB;AAED;;GAEG;AACH,wBAAgB,aAAa,IAAI,OAAO,CA+vFvC;AAED;;GAEG;AACH,wBAAsB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAG1C"}
|
package/dist/cli/index.js
CHANGED
|
@@ -17,7 +17,8 @@ import { createEricCountValidator, createEmtreeCountValidator } from "../query/v
|
|
|
17
17
|
import { createProviderInstance, executePreview, executeCountOnly, executeSearch } from "./commands/search-executor.js";
|
|
18
18
|
import { translateQueryCommand, formatTranslateResult } from "./commands/query/translate.js";
|
|
19
19
|
import { inspectQueryCommand, formatInspectOutput } from "./commands/query/inspect.js";
|
|
20
|
-
import { writeQueryTemplate
|
|
20
|
+
import { writeQueryTemplate } from "./commands/query/init.js";
|
|
21
|
+
import { resolveQueryFile } from "./commands/query/resolve.js";
|
|
21
22
|
import { getSessionDetails, computeDeduplicationStats, formatSessionDetails, listSessionsForDisplay, formatSessionList } from "./commands/status.js";
|
|
22
23
|
import { parseSearchOptions, validateSearchInput, formatShortKeywordWarning, formatDryRunOutput, formatPreviewOutput, formatCountOnlyOutput } from "./commands/search.js";
|
|
23
24
|
import { parseResumeOptions, validateResumeInput, getResumableProvidersForCommand } from "./commands/resume.js";
|
|
@@ -77,10 +78,10 @@ Workflow:
|
|
|
77
78
|
Iterate: search → results -q → check → diff Query refinement
|
|
78
79
|
|
|
79
80
|
Quick Start:
|
|
80
|
-
$ search-hub query init
|
|
81
|
-
$ search-hub search search
|
|
82
|
-
$ search-hub search search
|
|
83
|
-
$ search-hub results <session>
|
|
81
|
+
$ search-hub query init "my search" # Create query template
|
|
82
|
+
$ search-hub search my-search --count-only # Check hit counts
|
|
83
|
+
$ search-hub search my-search # Execute search
|
|
84
|
+
$ search-hub results <session> # Review titles`);
|
|
84
85
|
program.command("init").description("Initialize configuration directory").option("-f, --force", "overwrite existing configuration", false).addHelpText("after", `
|
|
85
86
|
Examples:
|
|
86
87
|
$ search-hub init # Initialize with default settings
|
|
@@ -190,9 +191,10 @@ Use "search-hub query init" to generate a template.`);
|
|
|
190
191
|
Examples:
|
|
191
192
|
$ search-hub query validate ./diabetes-ai.yaml
|
|
192
193
|
$ search-hub query validate ./diabetes-ai.yaml --no-vocab # Skip MeSH check
|
|
193
|
-
$ search-hub query validate ./diabetes-ai.yaml --no-cache # Ignore cache`).action(async (
|
|
194
|
+
$ search-hub query validate ./diabetes-ai.yaml --no-cache # Ignore cache`).action(async (fileArg, opts) => {
|
|
194
195
|
const globalOpts = program.opts();
|
|
195
196
|
try {
|
|
197
|
+
const file = await resolveQueryFile(fileArg);
|
|
196
198
|
const noVocab = opts.vocab === false;
|
|
197
199
|
const noCache = opts.cache === false;
|
|
198
200
|
let cache;
|
|
@@ -279,9 +281,10 @@ Examples:
|
|
|
279
281
|
queryCommand.command("translate").description("Show translated queries for each database").argument("<file>", "path to query YAML file").option("--db <provider>", "show translation for specific provider only").addHelpText("after", `
|
|
280
282
|
Examples:
|
|
281
283
|
$ search-hub query translate ./diabetes-ai.yaml # All databases
|
|
282
|
-
$ search-hub query translate ./diabetes-ai.yaml --db pubmed # PubMed only`).action(async (
|
|
284
|
+
$ search-hub query translate ./diabetes-ai.yaml --db pubmed # PubMed only`).action(async (fileArg, options) => {
|
|
283
285
|
const globalOpts = program.opts();
|
|
284
286
|
try {
|
|
287
|
+
const file = await resolveQueryFile(fileArg);
|
|
285
288
|
const translateOptions = options.db ? { providers: [options.db] } : {};
|
|
286
289
|
const result = await translateQueryCommand(file, translateOptions);
|
|
287
290
|
if (!globalOpts.quiet) {
|
|
@@ -301,9 +304,10 @@ Examples:
|
|
|
301
304
|
queryCommand.command("inspect").description("Show how a query resolves per provider (block replacements and added filters)").argument("<file>", "path to query YAML file").option("--db <provider>", "show resolution for specific provider only").addHelpText("after", `
|
|
302
305
|
Examples:
|
|
303
306
|
$ search-hub query inspect ./diabetes-ai.yaml # All databases
|
|
304
|
-
$ search-hub query inspect ./diabetes-ai.yaml --db pubmed # PubMed only`).action(async (
|
|
307
|
+
$ search-hub query inspect ./diabetes-ai.yaml --db pubmed # PubMed only`).action(async (fileArg, options) => {
|
|
305
308
|
const globalOpts = program.opts();
|
|
306
309
|
try {
|
|
310
|
+
const file = await resolveQueryFile(fileArg);
|
|
307
311
|
const inspectOptions = options.db ? { providers: [options.db] } : {};
|
|
308
312
|
const result = await inspectQueryCommand(file, inspectOptions);
|
|
309
313
|
if (!result.success) {
|
|
@@ -328,24 +332,35 @@ Examples:
|
|
|
328
332
|
process.exitCode = EXIT_CODES.QUERY_ERROR;
|
|
329
333
|
}
|
|
330
334
|
});
|
|
331
|
-
queryCommand.command("init").description("Generate a template query YAML file").option("-o, --output <path>", "write to file (
|
|
335
|
+
queryCommand.command("init").description("Generate a template query YAML file").argument("<title>", "query title (used for name field and filename)").option("-o, --output <path>", "write to specific file path").option("--stdout", "output to stdout instead of file").option("--force", "overwrite existing file", false).addHelpText("after", `
|
|
336
|
+
Examples:
|
|
337
|
+
$ search-hub query init "WBA pain mechanisms" # → queries/wba-pain-mechanisms.yaml
|
|
338
|
+
$ search-hub query init "WBA pain" -o ./custom-path.yaml # Custom output path
|
|
339
|
+
$ search-hub query init "WBA pain" --stdout # Print to stdout`).action(async (title, options) => {
|
|
332
340
|
const globalOpts = program.opts();
|
|
333
341
|
try {
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
342
|
+
const result = await writeQueryTemplate({
|
|
343
|
+
title,
|
|
344
|
+
output: options.output,
|
|
345
|
+
stdout: options.stdout,
|
|
346
|
+
force: options.force
|
|
347
|
+
});
|
|
348
|
+
if (!globalOpts.quiet) {
|
|
349
|
+
if (result.success) {
|
|
350
|
+
console.log(result.message);
|
|
351
|
+
if (result.outputPath) {
|
|
352
|
+
const suggestion = formatSuggestion(getSuggestion({
|
|
353
|
+
command: "query init",
|
|
354
|
+
outputFile: result.outputPath
|
|
355
|
+
}));
|
|
356
|
+
if (suggestion) console.log("\n" + suggestion);
|
|
357
|
+
console.log("\nIterate: edit the same file and re-run step 3. Counts are logged automatically.");
|
|
341
358
|
}
|
|
359
|
+
} else {
|
|
360
|
+
console.error(result.message);
|
|
342
361
|
}
|
|
343
|
-
process.exitCode = result.success ? EXIT_CODES.SUCCESS : EXIT_CODES.GENERAL_ERROR;
|
|
344
|
-
} else {
|
|
345
|
-
const template = generateQueryTemplate();
|
|
346
|
-
console.log(template);
|
|
347
|
-
process.exitCode = EXIT_CODES.SUCCESS;
|
|
348
362
|
}
|
|
363
|
+
process.exitCode = result.success ? EXIT_CODES.SUCCESS : EXIT_CODES.GENERAL_ERROR;
|
|
349
364
|
} catch (error) {
|
|
350
365
|
if (!globalOpts.quiet) {
|
|
351
366
|
console.error("Error:", error instanceof Error ? error.message : error);
|
|
@@ -356,9 +371,10 @@ Examples:
|
|
|
356
371
|
queryCommand.command("assess").description("Record an assessment of the current query iteration").argument("<file>", "path to query YAML file").option("--verdict <verdict>", "assessment verdict (e.g., reject, good, refine)").option("--precision <precision>", "estimated precision (e.g., ~60%)").option("--comment <comment>", "free-text comment").addHelpText("after", `
|
|
357
372
|
Examples:
|
|
358
373
|
$ search-hub query assess query.yaml --verdict reject --comment "Too broad"
|
|
359
|
-
$ search-hub query assess query.yaml --verdict good --precision "~60%"`).action(async (
|
|
374
|
+
$ search-hub query assess query.yaml --verdict good --precision "~60%"`).action(async (fileArg, options) => {
|
|
360
375
|
const globalOpts = program.opts();
|
|
361
376
|
try {
|
|
377
|
+
const file = await resolveQueryFile(fileArg);
|
|
362
378
|
const result = await executeQueryAssess(file, options);
|
|
363
379
|
if (result.success) {
|
|
364
380
|
if (!globalOpts.quiet) {
|
|
@@ -386,9 +402,10 @@ Examples:
|
|
|
386
402
|
queryCommand.command("log").description("View the query iteration history").argument("<file>", "path to query YAML file").option("--json", "output as JSON").addHelpText("after", `
|
|
387
403
|
Examples:
|
|
388
404
|
$ search-hub query log query.yaml
|
|
389
|
-
$ search-hub query log query.yaml --json`).action(async (
|
|
405
|
+
$ search-hub query log query.yaml --json`).action(async (fileArg, options) => {
|
|
390
406
|
const globalOpts = program.opts();
|
|
391
407
|
try {
|
|
408
|
+
const file = await resolveQueryFile(fileArg);
|
|
392
409
|
const entries = await readLogEntries(file);
|
|
393
410
|
if (!globalOpts.quiet) {
|
|
394
411
|
console.log(formatLogOutput(entries, { json: options?.json }));
|
|
@@ -468,7 +485,8 @@ Query features (use "query init" to see full template):
|
|
|
468
485
|
async (queryFile, options) => {
|
|
469
486
|
const globalOpts = program.opts();
|
|
470
487
|
try {
|
|
471
|
-
const
|
|
488
|
+
const resolvedQueryFile = queryFile ? await resolveQueryFile(queryFile) : void 0;
|
|
489
|
+
const searchOpts = parseSearchOptions(resolvedQueryFile, {
|
|
472
490
|
db: options?.db,
|
|
473
491
|
query: options?.query,
|
|
474
492
|
name: options?.name,
|
|
@@ -683,11 +701,14 @@ Warning: Some providers failed:
|
|
|
683
701
|
if (result.sessionId) {
|
|
684
702
|
const sessions = await listSessions(sessionsDir);
|
|
685
703
|
const suggestionCmd = searchOpts.directQuery ? "search --query" : "search";
|
|
704
|
+
const currentSession = sessions.find((s) => s.id === result.sessionId);
|
|
705
|
+
const previousSession = currentSession ? sessions.filter((s) => s.name === currentSession.name && s.id !== result.sessionId).sort((a, b) => b.createdAt.localeCompare(a.createdAt))[0] : void 0;
|
|
686
706
|
const suggestion = formatSuggestion(getSuggestion({
|
|
687
707
|
command: suggestionCmd,
|
|
688
708
|
sessionId: result.sessionId,
|
|
689
709
|
sessionStatus: result.sessionStatus,
|
|
690
|
-
sessionCount: sessions.length
|
|
710
|
+
sessionCount: sessions.length,
|
|
711
|
+
previousSessionId: previousSession?.id
|
|
691
712
|
}));
|
|
692
713
|
if (suggestion) console.log(suggestion);
|
|
693
714
|
}
|