@ncukondo/search-hub 0.22.0 → 0.23.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -7
- package/dist/cli/commands/config.d.ts +53 -0
- package/dist/cli/commands/config.d.ts.map +1 -1
- package/dist/cli/commands/config.js +62 -0
- package/dist/cli/commands/config.js.map +1 -1
- package/dist/cli/commands/fulltext/index.js +1 -0
- package/dist/cli/commands/fulltext/index.js.map +1 -1
- package/dist/cli/commands/init.d.ts +12 -17
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +116 -76
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/query/init.d.ts +0 -1
- package/dist/cli/commands/query/init.d.ts.map +1 -1
- package/dist/cli/commands/query/init.js +2 -3
- package/dist/cli/commands/query/init.js.map +1 -1
- package/dist/cli/commands/query/resolve.d.ts.map +1 -1
- package/dist/cli/commands/query/resolve.js +5 -2
- package/dist/cli/commands/query/resolve.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +183 -23
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/suggestions/rules.js +1 -1
- package/dist/cli/suggestions/rules.js.map +1 -1
- package/dist/config/index.d.ts +1 -0
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/loader.d.ts +4 -2
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/loader.js +7 -6
- package/dist/config/loader.js.map +1 -1
- package/dist/config/paths.d.ts +21 -0
- package/dist/config/paths.d.ts.map +1 -1
- package/dist/config/paths.js +28 -5
- package/dist/config/paths.js.map +1 -1
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/package.json.js +1 -1
- package/package.json +1 -1
|
@@ -3,8 +3,27 @@ import { join } from "node:path";
|
|
|
3
3
|
import { stringify } from "@iarna/toml";
|
|
4
4
|
import "../../config/schema.js";
|
|
5
5
|
import { getDefaultConfig } from "../../config/defaults.js";
|
|
6
|
-
import { getConfigDir,
|
|
6
|
+
import { getConfigDir, getProjectDir } from "../../config/paths.js";
|
|
7
7
|
import "node:os";
|
|
8
|
+
const FULL_PROVIDERS = ["pubmed", "eric", "arxiv", "scopus"];
|
|
9
|
+
const MINIMAL_PROVIDERS = ["wos", "embase"];
|
|
10
|
+
function buildProvidersToml(config) {
|
|
11
|
+
const providers = {};
|
|
12
|
+
for (const name of FULL_PROVIDERS) {
|
|
13
|
+
const p = config.providers[name];
|
|
14
|
+
providers[name] = {
|
|
15
|
+
enabled: p.enabled,
|
|
16
|
+
rate_limit: p.rate_limit,
|
|
17
|
+
timeout: p.timeout,
|
|
18
|
+
retries: p.retries,
|
|
19
|
+
max_results: p.max_results
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
for (const name of MINIMAL_PROVIDERS) {
|
|
23
|
+
providers[name] = { enabled: config.providers[name].enabled };
|
|
24
|
+
}
|
|
25
|
+
return providers;
|
|
26
|
+
}
|
|
8
27
|
async function exists(path) {
|
|
9
28
|
try {
|
|
10
29
|
await access(path, constants.F_OK);
|
|
@@ -13,11 +32,20 @@ async function exists(path) {
|
|
|
13
32
|
return false;
|
|
14
33
|
}
|
|
15
34
|
}
|
|
16
|
-
function
|
|
35
|
+
function localConfigToToml(config) {
|
|
17
36
|
return {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
37
|
+
providers: buildProvidersToml(config),
|
|
38
|
+
integration: {
|
|
39
|
+
reference_manager: {
|
|
40
|
+
enabled: config.integration.reference_manager.enabled,
|
|
41
|
+
command: config.integration.reference_manager.command,
|
|
42
|
+
auto_register: config.integration.reference_manager.auto_register
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function generateGlobalConfigContent(config) {
|
|
48
|
+
const tomlObj = {
|
|
21
49
|
log: {
|
|
22
50
|
level: config.log.level
|
|
23
51
|
},
|
|
@@ -25,55 +53,7 @@ function configToToml(config) {
|
|
|
25
53
|
color: config.output.color,
|
|
26
54
|
progress_bar: config.output.progress_bar
|
|
27
55
|
},
|
|
28
|
-
providers:
|
|
29
|
-
pubmed: {
|
|
30
|
-
enabled: config.providers.pubmed.enabled,
|
|
31
|
-
api_key: config.providers.pubmed.api_key ?? "",
|
|
32
|
-
email: config.providers.pubmed.email ?? "",
|
|
33
|
-
rate_limit: config.providers.pubmed.rate_limit,
|
|
34
|
-
timeout: config.providers.pubmed.timeout,
|
|
35
|
-
retries: config.providers.pubmed.retries,
|
|
36
|
-
max_results: config.providers.pubmed.max_results
|
|
37
|
-
},
|
|
38
|
-
eric: {
|
|
39
|
-
enabled: config.providers.eric.enabled,
|
|
40
|
-
rate_limit: config.providers.eric.rate_limit,
|
|
41
|
-
timeout: config.providers.eric.timeout,
|
|
42
|
-
retries: config.providers.eric.retries,
|
|
43
|
-
max_results: config.providers.eric.max_results
|
|
44
|
-
},
|
|
45
|
-
arxiv: {
|
|
46
|
-
enabled: config.providers.arxiv.enabled,
|
|
47
|
-
rate_limit: config.providers.arxiv.rate_limit,
|
|
48
|
-
timeout: config.providers.arxiv.timeout,
|
|
49
|
-
retries: config.providers.arxiv.retries,
|
|
50
|
-
max_results: config.providers.arxiv.max_results
|
|
51
|
-
},
|
|
52
|
-
scopus: {
|
|
53
|
-
enabled: config.providers.scopus.enabled,
|
|
54
|
-
api_key: config.providers.scopus.api_key ?? "",
|
|
55
|
-
inst_token: config.providers.scopus.inst_token ?? "",
|
|
56
|
-
rate_limit: config.providers.scopus.rate_limit,
|
|
57
|
-
timeout: config.providers.scopus.timeout,
|
|
58
|
-
retries: config.providers.scopus.retries,
|
|
59
|
-
max_results: config.providers.scopus.max_results
|
|
60
|
-
},
|
|
61
|
-
wos: {
|
|
62
|
-
enabled: config.providers.wos.enabled,
|
|
63
|
-
api_key: config.providers.wos.api_key ?? "",
|
|
64
|
-
rate_limit: config.providers.wos.rate_limit,
|
|
65
|
-
timeout: config.providers.wos.timeout,
|
|
66
|
-
retries: config.providers.wos.retries,
|
|
67
|
-
max_results: config.providers.wos.max_results
|
|
68
|
-
},
|
|
69
|
-
embase: {
|
|
70
|
-
enabled: config.providers.embase.enabled,
|
|
71
|
-
rate_limit: config.providers.embase.rate_limit,
|
|
72
|
-
timeout: config.providers.embase.timeout,
|
|
73
|
-
retries: config.providers.embase.retries,
|
|
74
|
-
max_results: config.providers.embase.max_results
|
|
75
|
-
}
|
|
76
|
-
},
|
|
56
|
+
providers: buildProvidersToml(config),
|
|
77
57
|
integration: {
|
|
78
58
|
reference_manager: {
|
|
79
59
|
enabled: config.integration.reference_manager.enabled,
|
|
@@ -82,54 +62,114 @@ function configToToml(config) {
|
|
|
82
62
|
}
|
|
83
63
|
}
|
|
84
64
|
};
|
|
85
|
-
|
|
86
|
-
function generateConfigContent(config) {
|
|
87
|
-
const tomlObj = configToToml(config);
|
|
88
|
-
const header = `# search-hub configuration file
|
|
65
|
+
const header = `# search-hub global configuration
|
|
89
66
|
# See: https://github.com/search-hub/search-hub for documentation
|
|
67
|
+
#
|
|
68
|
+
# [session]
|
|
69
|
+
# directory = "" # Override default session directory (empty = platform default)
|
|
70
|
+
|
|
71
|
+
`;
|
|
72
|
+
const tomlContent = stringify(tomlObj);
|
|
73
|
+
const lines = tomlContent.split("\n");
|
|
74
|
+
const result = [];
|
|
75
|
+
for (const line of lines) {
|
|
76
|
+
result.push(line);
|
|
77
|
+
if (line.startsWith("[providers.pubmed]")) {
|
|
78
|
+
result.push('# api_key = "" # Optional but recommended (NCBI E-utilities)');
|
|
79
|
+
result.push('# email = "" # Required by NCBI for tracking');
|
|
80
|
+
} else if (line.startsWith("[providers.scopus]")) {
|
|
81
|
+
result.push('# api_key = "" # Required for Scopus access');
|
|
82
|
+
result.push('# inst_token = "" # Optional institutional token');
|
|
83
|
+
} else if (line.startsWith("[providers.wos]")) {
|
|
84
|
+
result.push('# api_key = "" # Required for Web of Science');
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return header + result.join("\n");
|
|
88
|
+
}
|
|
89
|
+
function generateLocalConfigContent(config) {
|
|
90
|
+
const tomlObj = localConfigToToml(config);
|
|
91
|
+
const header = `# search-hub project configuration
|
|
92
|
+
# Project-specific overrides (no secrets - use env vars or global config for API keys)
|
|
90
93
|
|
|
91
94
|
`;
|
|
92
95
|
return header + stringify(tomlObj);
|
|
93
96
|
}
|
|
94
|
-
async function
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
} = options;
|
|
100
|
-
const configPath = join(configDir, "config.toml");
|
|
101
|
-
const sessionsDir = join(dataDir, "sessions");
|
|
102
|
-
const queriesDir = join(dataDir, "queries");
|
|
97
|
+
async function initLocal(directory, force) {
|
|
98
|
+
const projectDir = getProjectDir(directory);
|
|
99
|
+
const configPath = join(projectDir, "config.toml");
|
|
100
|
+
const sessionsDir = join(projectDir, "sessions");
|
|
101
|
+
const queriesDir = join(projectDir, "queries");
|
|
103
102
|
const result = {
|
|
104
103
|
success: false,
|
|
105
104
|
configPath,
|
|
106
|
-
|
|
107
|
-
configDir,
|
|
108
|
-
dataDir
|
|
105
|
+
projectDir
|
|
109
106
|
};
|
|
110
|
-
if (await exists(
|
|
107
|
+
if (await exists(projectDir)) {
|
|
111
108
|
if (!force) {
|
|
112
109
|
return {
|
|
113
110
|
...result,
|
|
114
111
|
alreadyExists: true,
|
|
115
|
-
message: `
|
|
112
|
+
message: `Project directory already exists at ${projectDir}. Use --force to overwrite.`
|
|
116
113
|
};
|
|
117
114
|
}
|
|
118
115
|
result.overwritten = true;
|
|
119
116
|
}
|
|
120
|
-
await mkdir(
|
|
117
|
+
await mkdir(projectDir, { recursive: true });
|
|
121
118
|
await mkdir(sessionsDir, { recursive: true });
|
|
122
119
|
await mkdir(queriesDir, { recursive: true });
|
|
123
120
|
const defaultConfig = getDefaultConfig();
|
|
124
|
-
|
|
125
|
-
const configContent = generateConfigContent(defaultConfig);
|
|
121
|
+
const configContent = generateLocalConfigContent(defaultConfig);
|
|
126
122
|
await writeFile(configPath, configContent, "utf-8");
|
|
127
123
|
return {
|
|
128
124
|
...result,
|
|
129
125
|
success: true,
|
|
130
|
-
message: result.overwritten ? `
|
|
126
|
+
message: result.overwritten ? `Project re-initialized at ${projectDir}` : `Project initialized at ${projectDir}`,
|
|
127
|
+
hints: [
|
|
128
|
+
"Set up global credentials: search-hub init --global",
|
|
129
|
+
"Or use environment variables: see search-hub config --env-vars",
|
|
130
|
+
"API keys can also be set via .env file in the project root"
|
|
131
|
+
]
|
|
131
132
|
};
|
|
132
133
|
}
|
|
134
|
+
async function initGlobal(configDir, force) {
|
|
135
|
+
const configPath = join(configDir, "config.toml");
|
|
136
|
+
const result = {
|
|
137
|
+
success: false,
|
|
138
|
+
configPath
|
|
139
|
+
};
|
|
140
|
+
if (await exists(configDir)) {
|
|
141
|
+
if (!force) {
|
|
142
|
+
return {
|
|
143
|
+
...result,
|
|
144
|
+
alreadyExists: true,
|
|
145
|
+
message: `Global configuration already exists at ${configDir}. Use --force to overwrite.`
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
result.overwritten = true;
|
|
149
|
+
}
|
|
150
|
+
await mkdir(configDir, { recursive: true });
|
|
151
|
+
const defaultConfig = getDefaultConfig();
|
|
152
|
+
const configContent = generateGlobalConfigContent(defaultConfig);
|
|
153
|
+
await writeFile(configPath, configContent, "utf-8");
|
|
154
|
+
return {
|
|
155
|
+
...result,
|
|
156
|
+
success: true,
|
|
157
|
+
message: result.overwritten ? `Global configuration overwritten at ${configDir}` : `Global configuration created at ${configDir}`,
|
|
158
|
+
hints: [
|
|
159
|
+
`Edit credentials: search-hub config --global set providers.pubmed.api_key <key>`,
|
|
160
|
+
`Or edit directly: ${configPath}`
|
|
161
|
+
]
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
async function init(options = {}) {
|
|
165
|
+
const { force = false } = options;
|
|
166
|
+
if (options.global) {
|
|
167
|
+
const configDir = options.configDir ?? getConfigDir();
|
|
168
|
+
return initGlobal(configDir, force);
|
|
169
|
+
}
|
|
170
|
+
const directory = options.directory ?? process.cwd();
|
|
171
|
+
return initLocal(directory, force);
|
|
172
|
+
}
|
|
133
173
|
export {
|
|
134
174
|
init
|
|
135
175
|
};
|
|
@@ -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 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
|
+
{"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, getProjectDir } from '../../config/paths.js';\nimport type { Config, ProviderConfig } from '../../config/index.js';\n\n/** Provider names that carry full settings (rate_limit, timeout, etc.) in generated configs. */\nconst FULL_PROVIDERS = ['pubmed', 'eric', 'arxiv', 'scopus'] as const;\n/** Provider names that only carry the enabled flag in generated configs. */\nconst MINIMAL_PROVIDERS = ['wos', 'embase'] as const;\n\n/**\n * Build the providers TOML object from a Config, excluding secrets.\n */\nfunction buildProvidersToml(config: Config): Record<string, Partial<ProviderConfig>> {\n const providers: Record<string, Partial<ProviderConfig>> = {};\n for (const name of FULL_PROVIDERS) {\n const p = config.providers[name];\n providers[name] = {\n enabled: p.enabled,\n rate_limit: p.rate_limit,\n timeout: p.timeout,\n retries: p.retries,\n max_results: p.max_results,\n };\n }\n for (const name of MINIMAL_PROVIDERS) {\n providers[name] = { enabled: config.providers[name].enabled };\n }\n return providers;\n}\n\n/**\n * Options for the init command.\n */\nexport interface InitOptions {\n /** Directory to create .search-hub/ in (defaults to cwd) */\n directory?: string;\n /** Initialize global config instead of local project */\n global?: boolean;\n /** Config directory for global init (defaults to platform-specific via getConfigDir()) */\n configDir?: 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 .search-hub/ project directory (local init only) */\n projectDir?: 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 /** Actionable hints for the user */\n hints?: 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 for local project config.\n * Excludes secrets (api_key, email, inst_token).\n */\nfunction localConfigToToml(config: Config): Record<string, unknown> {\n return {\n providers: buildProvidersToml(config),\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 content for global config with credential hints as comments.\n */\nfunction generateGlobalConfigContent(config: Config): string {\n const tomlObj = {\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: buildProvidersToml(config),\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 const header = `# search-hub global configuration\n# See: https://github.com/search-hub/search-hub for documentation\n#\n# [session]\n# directory = \"\" # Override default session directory (empty = platform default)\n\n`;\n\n const tomlContent = stringifyToml(tomlObj as Parameters<typeof stringifyToml>[0]);\n\n // Add credential hints as comments after provider sections\n const lines = tomlContent.split('\\n');\n const result: string[] = [];\n for (const line of lines) {\n result.push(line);\n if (line.startsWith('[providers.pubmed]')) {\n result.push('# api_key = \"\" # Optional but recommended (NCBI E-utilities)');\n result.push('# email = \"\" # Required by NCBI for tracking');\n } else if (line.startsWith('[providers.scopus]')) {\n result.push('# api_key = \"\" # Required for Scopus access');\n result.push('# inst_token = \"\" # Optional institutional token');\n } else if (line.startsWith('[providers.wos]')) {\n result.push('# api_key = \"\" # Required for Web of Science');\n }\n }\n\n return header + result.join('\\n');\n}\n\n/**\n * Generate TOML config file content for local project config.\n */\nfunction generateLocalConfigContent(config: Config): string {\n const tomlObj = localConfigToToml(config);\n const header = `# search-hub project configuration\n# Project-specific overrides (no secrets - use env vars or global config for API keys)\n\n`;\n return header + stringifyToml(tomlObj as Parameters<typeof stringifyToml>[0]);\n}\n\n/**\n * Initialize a local .search-hub/ project directory.\n */\nasync function initLocal(directory: string, force: boolean): Promise<InitResult> {\n const projectDir = getProjectDir(directory);\n const configPath = join(projectDir, 'config.toml');\n const sessionsDir = join(projectDir, 'sessions');\n const queriesDir = join(projectDir, 'queries');\n\n const result: InitResult = {\n success: false,\n configPath,\n projectDir,\n };\n\n // Check if .search-hub/ already exists\n if (await exists(projectDir)) {\n if (!force) {\n return {\n ...result,\n alreadyExists: true,\n message: `Project directory already exists at ${projectDir}. Use --force to overwrite.`,\n };\n }\n result.overwritten = true;\n }\n\n // Create directories\n await mkdir(projectDir, { recursive: true });\n await mkdir(sessionsDir, { recursive: true });\n await mkdir(queriesDir, { recursive: true });\n\n // Generate and write local config (no secrets)\n const defaultConfig = getDefaultConfig();\n const configContent = generateLocalConfigContent(defaultConfig);\n await writeFile(configPath, configContent, 'utf-8');\n\n return {\n ...result,\n success: true,\n message: result.overwritten\n ? `Project re-initialized at ${projectDir}`\n : `Project initialized at ${projectDir}`,\n hints: [\n 'Set up global credentials: search-hub init --global',\n 'Or use environment variables: see search-hub config --env-vars',\n 'API keys can also be set via .env file in the project root',\n ],\n };\n}\n\n/**\n * Initialize the global search-hub configuration.\n */\nasync function initGlobal(configDir: string, force: boolean): Promise<InitResult> {\n const configPath = join(configDir, 'config.toml');\n\n const result: InitResult = {\n success: false,\n configPath,\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: `Global configuration already exists at ${configDir}. Use --force to overwrite.`,\n };\n }\n result.overwritten = true;\n }\n\n // Create directory\n await mkdir(configDir, { recursive: true });\n\n // Generate and write global config with credential hints\n const defaultConfig = getDefaultConfig();\n const configContent = generateGlobalConfigContent(defaultConfig);\n await writeFile(configPath, configContent, 'utf-8');\n\n return {\n ...result,\n success: true,\n message: result.overwritten\n ? `Global configuration overwritten at ${configDir}`\n : `Global configuration created at ${configDir}`,\n hints: [\n `Edit credentials: search-hub config --global set providers.pubmed.api_key <key>`,\n `Or edit directly: ${configPath}`,\n ],\n };\n}\n\n/**\n * Initialize search-hub configuration.\n *\n * By default, creates a `.search-hub/` project directory in the specified directory (or cwd).\n * With `--global`, creates the global config at the XDG-compliant path.\n */\nexport async function init(options: InitOptions = {}): Promise<InitResult> {\n const { force = false } = options;\n\n if (options.global) {\n const configDir = options.configDir ?? getConfigDir();\n return initGlobal(configDir, force);\n }\n\n const directory = options.directory ?? process.cwd();\n return initLocal(directory, force);\n}\n"],"names":["stringifyToml"],"mappings":"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"}
|
|
@@ -14,7 +14,6 @@ export declare const QUERY_SCHEMA_FILENAME = "query.schema.json";
|
|
|
14
14
|
* @returns The YAML template string with comments
|
|
15
15
|
*/
|
|
16
16
|
export declare function generateQueryTemplate(title?: string): string;
|
|
17
|
-
export declare const QUERIES_DIR = "queries";
|
|
18
17
|
export interface WriteQueryTemplateOptions {
|
|
19
18
|
title: string;
|
|
20
19
|
output?: string | undefined;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/query/init.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/query/init.ts"],"names":[],"mappings":"AAUA;;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;AAGD,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,7 @@
|
|
|
1
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
|
+
import { getQueriesDir } from "../../../config/paths.js";
|
|
4
5
|
function sanitizeForFilename(title) {
|
|
5
6
|
const result = title.trim().toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9_-]/g, "").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
6
7
|
if (!result) {
|
|
@@ -69,13 +70,12 @@ function generateQueryTemplate(title) {
|
|
|
69
70
|
const escaped = title.replace(/"/g, '\\"');
|
|
70
71
|
return QUERY_TEMPLATE.replace("name: my_search", `name: "${escaped}"`);
|
|
71
72
|
}
|
|
72
|
-
const QUERIES_DIR = "queries";
|
|
73
73
|
async function writeQueryTemplate(options) {
|
|
74
74
|
const template = generateQueryTemplate(options.title);
|
|
75
75
|
if (options.stdout) {
|
|
76
76
|
return { success: true, message: template };
|
|
77
77
|
}
|
|
78
|
-
const outputPath = options.output ?? join(options.cwd
|
|
78
|
+
const outputPath = options.output ?? join(getQueriesDir(options.cwd), `${sanitizeForFilename(options.title)}.yaml`);
|
|
79
79
|
if (!options.force) {
|
|
80
80
|
try {
|
|
81
81
|
await access(outputPath);
|
|
@@ -98,7 +98,6 @@ async function writeQueryTemplate(options) {
|
|
|
98
98
|
};
|
|
99
99
|
}
|
|
100
100
|
export {
|
|
101
|
-
QUERIES_DIR,
|
|
102
101
|
QUERY_SCHEMA_FILENAME,
|
|
103
102
|
generateQueryTemplate,
|
|
104
103
|
sanitizeForFilename,
|
|
@@ -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, 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\
|
|
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\";\nimport { getQueriesDir } from \"../../../config/paths.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\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(getQueriesDir(options.cwd), `${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":";;;;AAaO,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;AAmBA,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,cAAc,QAAQ,GAAG,GAAG,GAAG,oBAAoB,QAAQ,KAAK,CAAC,OAAO;AAGlF,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;"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resolve.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/query/resolve.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"resolve.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/query/resolve.ts"],"names":[],"mappings":"AAcA,qBAAa,aAAc,SAAQ,KAAK;gBAC1B,IAAI,EAAE,MAAM;CAIzB;AAiBD,wBAAsB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CA2CnE"}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { stat } from "node:fs/promises";
|
|
2
|
+
import { relative } from "node:path";
|
|
3
|
+
import { getQueriesDir } from "../../../config/paths.js";
|
|
2
4
|
class NotAFileError extends Error {
|
|
3
5
|
constructor(path) {
|
|
4
6
|
super(`Path is not a file: ${path}`);
|
|
@@ -31,14 +33,15 @@ async function resolveQueryFile(arg) {
|
|
|
31
33
|
return withExt;
|
|
32
34
|
}
|
|
33
35
|
}
|
|
36
|
+
const queriesDir = relative(process.cwd(), getQueriesDir());
|
|
34
37
|
const basename = arg.endsWith(".yaml") || arg.endsWith(".yml") ? arg : `${arg}.yaml`;
|
|
35
|
-
const inQueries =
|
|
38
|
+
const inQueries = `${queriesDir}/${basename}`;
|
|
36
39
|
candidates.push(inQueries);
|
|
37
40
|
if (await isFile(inQueries)) {
|
|
38
41
|
return inQueries;
|
|
39
42
|
}
|
|
40
43
|
if (!arg.endsWith(".yaml") && !arg.endsWith(".yml")) {
|
|
41
|
-
const inQueriesYml =
|
|
44
|
+
const inQueriesYml = `${queriesDir}/${arg}.yml`;
|
|
42
45
|
candidates.push(inQueriesYml);
|
|
43
46
|
if (await isFile(inQueriesYml)) {
|
|
44
47
|
return inQueriesYml;
|
|
@@ -1 +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 =
|
|
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. .search-hub/queries/<arg>.yaml exists → use it\n * 4. .search-hub/queries/<arg>.yml exists → use it\n * 5. Error with tried paths\n */\nimport { stat } from 'node:fs/promises';\nimport { relative } from 'node:path';\nimport { getQueriesDir } from '../../../config/paths.js';\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. .search-hub/queries/<arg>.yaml\n const queriesDir = relative(process.cwd(), getQueriesDir());\n const basename = arg.endsWith('.yaml') || arg.endsWith('.yml') ? arg : `${arg}.yaml`;\n const inQueries = `${queriesDir}/${basename}`;\n candidates.push(inQueries);\n if (await isFile(inQueries)) {\n return inQueries;\n }\n\n // 4. .search-hub/queries/<arg>.yml (skip if arg already has extension)\n if (!arg.endsWith('.yaml') && !arg.endsWith('.yml')) {\n const inQueriesYml = `${queriesDir}/${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":";;;AAcO,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,aAAa,SAAS,QAAQ,IAAA,GAAO,eAAe;AAC1D,QAAM,WAAW,IAAI,SAAS,OAAO,KAAK,IAAI,SAAS,MAAM,IAAI,MAAM,GAAG,GAAG;AAC7E,QAAM,YAAY,GAAG,UAAU,IAAI,QAAQ;AAC3C,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,GAAG,UAAU,IAAI,GAAG;AACzC,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;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":";AAQA,OAAO,EAAE,OAAO,EAAU,MAAM,WAAW,CAAC;AAoN5C;;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,CAg8FvC;AAED;;GAEG;AACH,wBAAsB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAG1C"}
|