@ncukondo/search-hub 0.20.1 → 0.22.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 CHANGED
@@ -21,6 +21,26 @@ A CLI tool for systematic literature searching across multiple academic database
21
21
 
22
22
  ## Installation
23
23
 
24
+ ### Binary (no Node.js required)
25
+
26
+ Download the latest binary for your platform:
27
+
28
+ **Linux / macOS (Intel & Apple Silicon):**
29
+
30
+ ```bash
31
+ curl -fsSL https://raw.githubusercontent.com/ncukondo/search-hub/main/install.sh | bash
32
+ ```
33
+
34
+ **Windows (PowerShell):**
35
+
36
+ ```powershell
37
+ irm https://raw.githubusercontent.com/ncukondo/search-hub/main/install.ps1 | iex
38
+ ```
39
+
40
+ Or download manually from [GitHub Releases](https://github.com/ncukondo/search-hub/releases).
41
+
42
+ ### npm
43
+
24
44
  ```bash
25
45
  npm install -g @ncukondo/search-hub
26
46
  ```
@@ -44,14 +64,14 @@ This creates config and data directories in platform-specific locations:
44
64
 
45
65
  2. Create a query file:
46
66
  ```bash
47
- search-hub query init -o query.yaml
67
+ search-hub query init "my review"
48
68
  ```
49
69
 
50
- This generates a YAML template with JSON Schema support for editor autocompletion. Edit it to define your search:
70
+ This creates `queries/my-review.yaml` with JSON Schema support for editor autocompletion. Edit it to define your search:
51
71
 
52
72
  ```yaml
53
73
  # yaml-language-server: $schema=./query.schema.json
54
- name: my_review
74
+ name: "my review"
55
75
  description: "Literature search for scoping review"
56
76
 
57
77
  query:
@@ -70,14 +90,14 @@ filters:
70
90
 
71
91
  3. Validate the query:
72
92
  ```bash
73
- search-hub query validate query.yaml
93
+ search-hub query validate my-review
74
94
  ```
75
95
 
76
- This checks structure, validates controlled vocabulary terms (MeSH, ERIC descriptors, Emtree) against external APIs, and suggests corrections for typos.
96
+ 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
97
 
78
98
  4. Run search:
79
99
  ```bash
80
- search-hub search query.yaml
100
+ search-hub search my-review
81
101
  ```
82
102
 
83
103
  5. Export results:
@@ -91,31 +111,33 @@ Developing an effective search query is iterative. Start broad, then refine base
91
111
 
92
112
  ### Workflow
93
113
 
94
- 1. **Start with a broad query** - Get an initial set of results:
114
+ 1. **Create a query** - Start with a template:
95
115
  ```bash
96
- search-hub search query-v1.yaml --max-results 100
116
+ search-hub query init "my review"
117
+ # Creates queries/my-review.yaml
97
118
  ```
98
119
 
99
- 2. **Review initial results** - Check titles to assess quality:
120
+ 2. **Check hit counts** - Preview before downloading:
100
121
  ```bash
101
- search-hub results <session-v1> --limit 50
102
- search-hub results <session-v1> -q "title:diabetes year:2023-2025"
122
+ search-hub search my-review --count-only
103
123
  ```
104
124
 
105
- 3. **Check coverage** - Verify known relevant articles are captured:
125
+ 3. **Run the search** - When counts look good:
106
126
  ```bash
107
- search-hub check <session-v1> --file known-articles.txt
127
+ search-hub search my-review
108
128
  ```
109
129
 
110
- 4. **Refine the query** - Copy and modify your query file:
130
+ 4. **Review results** - Check titles to assess quality:
111
131
  ```bash
112
- cp query-v1.yaml query-v2.yaml
113
- # Edit query-v2.yaml to add/remove terms, adjust filters
132
+ search-hub results <session-id> --limit 50
133
+ search-hub results <session-id> -q "title:diabetes year:2023-2025"
114
134
  ```
115
135
 
116
- 5. **Run the refined search**:
136
+ 5. **Refine and re-run** - Edit the query file, then iterate:
117
137
  ```bash
118
- search-hub search query-v2.yaml --max-results 100
138
+ $EDITOR queries/my-review.yaml
139
+ search-hub search my-review --count-only # Re-check counts
140
+ search-hub search my-review # Execute full search
119
141
  ```
120
142
 
121
143
  6. **Compare results with diff** - See what changed:
@@ -128,22 +150,22 @@ Developing an effective search query is iterative. Start broad, then refine base
128
150
 
129
151
  - **Use `--count-only` first**: Check hit counts before downloading full results.
130
152
  ```bash
131
- search-hub search query.yaml --count-only
153
+ search-hub search my-review --count-only
132
154
  ```
133
155
 
134
156
  - **Use `--preview`** to see hit counts with sample titles:
135
157
  ```bash
136
- search-hub search query.yaml --preview
158
+ search-hub search my-review --preview
137
159
  ```
138
160
 
139
161
  - **Use `--dry-run`** to preview translations: See exactly what query each database will receive.
140
162
  ```bash
141
- search-hub search query.yaml --dry-run
163
+ search-hub search my-review --dry-run
142
164
  ```
143
165
 
144
166
  - **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
167
 
146
- - **Keep query versions**: Save each iteration (v1, v2, v3) to track your development process and maintain reproducibility.
168
+ - **Track iterations**: Use `query assess` and `query log` to record and review your refinement history.
147
169
 
148
170
  ## Fulltext Retrieval
149
171
 
@@ -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,CAiDzE"}
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
- * @param options - Output options
16
- * @param options.output - File path to write to (if omitted, returns template in message)
17
- * @param options.force - Whether to overwrite existing files
18
- * @returns Result with success status and message
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;;;;GAIG;AACH,wBAAgB,qBAAqB,IAAI,MAAM,CAE9C;AAED;;;;;;;GAOG;AACH,wBAAsB,kBAAkB,CAAC,OAAO,EAAE;IAChD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB,GAAG,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,CAkCjD"}
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 (!options.output) {
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(options.output);
81
+ await access(outputPath);
71
82
  return {
72
83
  success: false,
73
- message: `File already exists: ${options.output}. Use --force to overwrite.`
84
+ message: `File already exists: ${outputPath}. Use --force to overwrite.`
74
85
  };
75
86
  } catch {
76
87
  }
77
88
  }
78
- await writeFile(options.output, template, "utf-8");
79
- const schemaPath = join(dirname(options.output), QUERY_SCHEMA_FILENAME);
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: `Template written to ${options.output}`
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 * @param options - Output options\n * @param options.output - File path to write to (if omitted, returns template in message)\n * @param options.force - Whether to overwrite existing files\n * @returns Result with success status and message\n */\nexport async function writeQueryTemplate(options: {\n output?: string;\n force?: boolean;\n}): Promise<{ success: boolean; message: string }> {\n const template = generateQueryTemplate();\n\n if (!options.output) {\n // No output file specified, return template as message\n return { success: true, message: template };\n }\n\n // Check if file exists (unless force is set)\n if (!options.force) {\n try {\n await access(options.output);\n // File exists and force is not set\n return {\n success: false,\n message: `File already exists: ${options.output}. Use --force to overwrite.`,\n };\n } catch {\n // File does not exist, proceed\n }\n }\n\n // Write to file\n await fsWriteFile(options.output, template, \"utf-8\");\n\n // Generate JSON Schema file alongside output\n const schemaPath = join(dirname(options.output), 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: `Template written to ${options.output}`,\n };\n}\n"],"names":["fsWriteFile"],"mappings":";;;AAaO,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;AA4DrD,SAAS,wBAAgC;AAC9C,SAAO;AACT;AAUA,eAAsB,mBAAmB,SAGU;AACjD,QAAM,WAAW,sBAAA;AAEjB,MAAI,CAAC,QAAQ,QAAQ;AAEnB,WAAO,EAAE,SAAS,MAAM,SAAS,SAAA;AAAA,EACnC;AAGA,MAAI,CAAC,QAAQ,OAAO;AAClB,QAAI;AACF,YAAM,OAAO,QAAQ,MAAM;AAE3B,aAAO;AAAA,QACL,SAAS;AAAA,QACT,SAAS,wBAAwB,QAAQ,MAAM;AAAA,MAAA;AAAA,IAEnD,QAAQ;AAAA,IAER;AAAA,EACF;AAGA,QAAMA,UAAY,QAAQ,QAAQ,UAAU,OAAO;AAGnD,QAAM,aAAa,KAAK,QAAQ,QAAQ,MAAM,GAAG,qBAAqB;AACtE,QAAM,aAAa,wBAAA;AACnB,QAAMA,UAAY,YAAY,KAAK,UAAU,YAAY,MAAM,CAAC,IAAI,MAAM,OAAO;AAEjF,SAAO;AAAA,IACL,SAAS;AAAA,IACT,SAAS,uBAAuB,QAAQ,MAAM;AAAA,EAAA;AAElD;"}
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,5 @@
1
+ export declare class NotAFileError extends Error {
2
+ constructor(path: string);
3
+ }
4
+ export declare function resolveQueryFile(arg: string): Promise<string>;
5
+ //# sourceMappingURL=resolve.d.ts.map
@@ -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;"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=entry-bun.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entry-bun.d.ts","sourceRoot":"","sources":["../../src/cli/entry-bun.ts"],"names":[],"mappings":""}
@@ -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,CAiuFvC;AAED;;GAEG;AACH,wBAAsB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAG1C"}
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"}