@ncukondo/search-hub 0.21.0 → 0.23.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.
@@ -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, getDataDir } from "../../config/paths.js";
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 configToToml(config) {
35
+ function localConfigToToml(config) {
17
36
  return {
18
- session: {
19
- directory: config.session.directory
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 init(options = {}) {
95
- const {
96
- configDir = getConfigDir(),
97
- dataDir = getDataDir(),
98
- force = false
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
- sessionsDir,
107
- configDir,
108
- dataDir
105
+ projectDir
109
106
  };
110
- if (await exists(configDir)) {
107
+ if (await exists(projectDir)) {
111
108
  if (!force) {
112
109
  return {
113
110
  ...result,
114
111
  alreadyExists: true,
115
- message: `Configuration directory already exists at ${configDir}. Use --force to overwrite.`
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(configDir, { recursive: true });
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
- defaultConfig.session.directory = sessionsDir;
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 ? `Configuration overwritten at ${configDir}` : `Configuration created at ${configDir}`
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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"}
@@ -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,CA+vFvC;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;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"}
package/dist/cli/index.js CHANGED
@@ -4,11 +4,12 @@ import { Command, Option } from "commander";
4
4
  import { VERSION } from "../version.js";
5
5
  import { init } from "./commands/init.js";
6
6
  import { EXIT_CODES } from "./exit-codes.js";
7
- import { loadConfig, saveConfig } from "../config/loader.js";
7
+ import { loadConfig, loadTomlFile, saveConfig } from "../config/loader.js";
8
8
  import "../config/schema.js";
9
9
  import { getDefaultConfig } from "../config/defaults.js";
10
- import { getDefaultConfigPath } from "../config/paths.js";
11
- import { viewConfig, viewConfigKey, setConfigKey } from "./commands/config.js";
10
+ import { isInsideProject, getDefaultConfigPath, getLocalConfigPath } from "../config/paths.js";
11
+ import { formatEnvVars, viewConfigAllOrigins, viewConfigFiltered, viewConfig, formatShowOrigin, getNestedValue, viewConfigKey, resolveWriteScope, checkSecretKeyWarning, setConfigKey, parseValue, setNestedValue } from "./commands/config.js";
12
+ import { ENV_VAR_MAP } from "../config/env.js";
12
13
  import { detectSchemaLink, validateQueryCommand, formatValidateResult, formatVocabValidationOutput, hasVocabErrors } from "./commands/query/validate.js";
13
14
  import { MeSHLookupClient } from "../query/mesh-lookup.js";
14
15
  import { RateLimiter } from "../providers/base/rate-limiter.js";
@@ -82,16 +83,23 @@ Quick Start:
82
83
  $ search-hub search my-search --count-only # Check hit counts
83
84
  $ search-hub search my-search # Execute search
84
85
  $ search-hub results <session> # Review titles`);
85
- program.command("init").description("Initialize configuration directory").option("-f, --force", "overwrite existing configuration", false).addHelpText("after", `
86
+ program.command("init").description("Initialize search-hub project (local) or global config").option("-f, --force", "overwrite existing configuration", false).option("-g, --global", "initialize global config instead of local project", false).addHelpText("after", `
86
87
  Examples:
87
- $ search-hub init # Initialize with default settings
88
+ $ search-hub init # Create .search-hub/ in current directory
89
+ $ search-hub init --global # Create global config (~/.config/search-hub/)
88
90
  $ search-hub init --force # Overwrite existing configuration`).action(async (options) => {
89
91
  const globalOpts = program.opts();
90
92
  try {
91
- const result = await init({ force: options.force });
93
+ const result = await init({ force: options.force, global: options.global });
92
94
  if (!globalOpts.quiet) {
93
95
  if (result.success) {
94
96
  console.log(result.message);
97
+ if (result.hints) {
98
+ console.log("");
99
+ for (const hint of result.hints) {
100
+ console.log(` ${hint}`);
101
+ }
102
+ }
95
103
  } else {
96
104
  console.error(result.message);
97
105
  }
@@ -107,13 +115,91 @@ Examples:
107
115
  process.exitCode = EXIT_CODES.GENERAL_ERROR;
108
116
  }
109
117
  });
110
- program.command("config").description("View and edit configuration").argument("[key]", "configuration key to view or set").argument("[value]", "value to set for the key").addHelpText("after", `
118
+ program.command("config").description("View and edit configuration").argument("[key]", "configuration key to view or set").argument("[value]", "value to set for the key").option("--global", "Use global config scope").option("--local", "Use local project config scope").option("--show-origin", "Show where each config value comes from").option("--list", "List all config values (default when no key given)").option("--env-vars", "Show environment variable mappings").option("--force", "Force write even for secret keys in local scope").addHelpText("after", `
111
119
  Examples:
112
- $ search-hub config # Show all config
113
- $ search-hub config providers.pubmed # Show PubMed config
114
- $ search-hub config providers.pubmed.api_key KEY # Set API key`).action(async (key, value) => {
120
+ $ search-hub config # Show all config
121
+ $ search-hub config providers.pubmed # Show PubMed config
122
+ $ search-hub config --global providers.pubmed.api_key KEY # Set API key globally
123
+ $ search-hub config --local output.color false # Set in project config
124
+ $ search-hub config --show-origin providers.pubmed.api_key # Show value origin
125
+ $ search-hub config --list --global # Show only global values
126
+ $ search-hub config --env-vars # Show env var mappings`).action(async (key, value, cmdOpts) => {
115
127
  const globalOpts = program.opts();
116
128
  try {
129
+ if (cmdOpts.envVars) {
130
+ if (!globalOpts.quiet) {
131
+ console.log(formatEnvVars());
132
+ }
133
+ process.exitCode = EXIT_CODES.SUCCESS;
134
+ return;
135
+ }
136
+ const inProject = await isInsideProject();
137
+ if (cmdOpts.list || !key && !value) {
138
+ if (cmdOpts.global && cmdOpts.local) {
139
+ if (!globalOpts.quiet) {
140
+ console.error("Error: --global and --local are mutually exclusive");
141
+ }
142
+ process.exitCode = EXIT_CODES.CONFIG_ERROR;
143
+ return;
144
+ }
145
+ if (cmdOpts.showOrigin && !cmdOpts.global && !cmdOpts.local) {
146
+ let config22;
147
+ try {
148
+ config22 = await loadConfig(
149
+ globalOpts.config ? { explicitConfigPath: globalOpts.config } : {}
150
+ );
151
+ } catch {
152
+ config22 = getDefaultConfig();
153
+ }
154
+ const globalPath = expandPath(getDefaultConfigPath());
155
+ const globalCfg = await loadTomlFile(globalPath);
156
+ const localPath = inProject ? getLocalConfigPath() : "";
157
+ const localCfg = inProject ? await loadTomlFile(localPath) : {};
158
+ if (!globalOpts.quiet) {
159
+ console.log(viewConfigAllOrigins(
160
+ config22,
161
+ ENV_VAR_MAP,
162
+ localCfg,
163
+ localPath,
164
+ globalCfg,
165
+ globalPath
166
+ ));
167
+ }
168
+ } else if (cmdOpts.global) {
169
+ const globalConfig = await loadTomlFile(expandPath(getDefaultConfigPath()));
170
+ if (!globalOpts.quiet) {
171
+ const output = viewConfigFiltered(globalConfig);
172
+ console.log(output || "(no global config values set)");
173
+ }
174
+ } else if (cmdOpts.local) {
175
+ if (!inProject) {
176
+ if (!globalOpts.quiet) {
177
+ console.error('Error: --local requires a project directory (.search-hub/). Run "search-hub init" first.');
178
+ }
179
+ process.exitCode = EXIT_CODES.CONFIG_ERROR;
180
+ return;
181
+ }
182
+ const localConfig = await loadTomlFile(getLocalConfigPath());
183
+ if (!globalOpts.quiet) {
184
+ const output = viewConfigFiltered(localConfig);
185
+ console.log(output || "(no local config values set)");
186
+ }
187
+ } else {
188
+ let config22;
189
+ try {
190
+ config22 = await loadConfig(
191
+ globalOpts.config ? { explicitConfigPath: globalOpts.config } : {}
192
+ );
193
+ } catch {
194
+ config22 = getDefaultConfig();
195
+ }
196
+ if (!globalOpts.quiet) {
197
+ console.log(viewConfig(config22));
198
+ }
199
+ }
200
+ process.exitCode = EXIT_CODES.SUCCESS;
201
+ return;
202
+ }
117
203
  let config2;
118
204
  try {
119
205
  config2 = await loadConfig(
@@ -122,29 +208,103 @@ Examples:
122
208
  } catch {
123
209
  config2 = getDefaultConfig();
124
210
  }
125
- if (!key) {
126
- if (!globalOpts.quiet) {
127
- console.log(viewConfig(config2));
211
+ if (key && !value) {
212
+ if (cmdOpts.showOrigin) {
213
+ const envEntry = Object.entries(ENV_VAR_MAP).find(([, path]) => path === key);
214
+ if (envEntry && process.env[envEntry[0]] !== void 0) {
215
+ if (!globalOpts.quiet) {
216
+ console.log(formatShowOrigin(key, process.env[envEntry[0]], "env", envEntry[0]));
217
+ }
218
+ process.exitCode = EXIT_CODES.SUCCESS;
219
+ return;
220
+ }
221
+ if (inProject) {
222
+ const localConfig = await loadTomlFile(getLocalConfigPath());
223
+ const localVal = getNestedValue(localConfig, key);
224
+ if (localVal !== void 0) {
225
+ if (!globalOpts.quiet) {
226
+ console.log(formatShowOrigin(key, String(localVal), "local", getLocalConfigPath()));
227
+ }
228
+ process.exitCode = EXIT_CODES.SUCCESS;
229
+ return;
230
+ }
231
+ }
232
+ const globalConfigPath = expandPath(getDefaultConfigPath());
233
+ const globalConfig = await loadTomlFile(globalConfigPath);
234
+ const globalVal = getNestedValue(globalConfig, key);
235
+ if (globalVal !== void 0) {
236
+ if (!globalOpts.quiet) {
237
+ console.log(formatShowOrigin(key, String(globalVal), "global", globalConfigPath));
238
+ }
239
+ process.exitCode = EXIT_CODES.SUCCESS;
240
+ return;
241
+ }
242
+ const mergedVal = getNestedValue(config2, key);
243
+ if (mergedVal !== void 0) {
244
+ if (!globalOpts.quiet) {
245
+ console.log(formatShowOrigin(key, String(mergedVal), "default", ""));
246
+ }
247
+ } else {
248
+ if (!globalOpts.quiet) {
249
+ console.error(`Error: Key "${key}" not found in configuration`);
250
+ }
251
+ process.exitCode = EXIT_CODES.CONFIG_ERROR;
252
+ return;
253
+ }
254
+ } else {
255
+ const result = viewConfigKey(config2, key);
256
+ if (result.success) {
257
+ if (!globalOpts.quiet) {
258
+ console.log(result.value);
259
+ }
260
+ } else {
261
+ if (!globalOpts.quiet) {
262
+ console.error(`Error: ${result.error}`);
263
+ }
264
+ process.exitCode = EXIT_CODES.CONFIG_ERROR;
265
+ return;
266
+ }
128
267
  }
129
- } else if (!value) {
130
- const result = viewConfigKey(config2, key);
131
- if (result.success) {
268
+ } else if (key && value) {
269
+ const scopeResult = resolveWriteScope({
270
+ global: !!cmdOpts.global,
271
+ local: !!cmdOpts.local,
272
+ insideProject: inProject
273
+ });
274
+ if (scopeResult.scope === "error") {
132
275
  if (!globalOpts.quiet) {
133
- console.log(result.value);
276
+ console.error(`Error: ${scopeResult.error}`);
134
277
  }
135
- } else {
278
+ process.exitCode = EXIT_CODES.CONFIG_ERROR;
279
+ return;
280
+ }
281
+ const warning = checkSecretKeyWarning(key, scopeResult.scope);
282
+ if (warning && !cmdOpts.force) {
136
283
  if (!globalOpts.quiet) {
137
- console.error(`Error: ${result.error}`);
284
+ console.error(`Error: ${warning} Use --force to override.`);
138
285
  }
139
286
  process.exitCode = EXIT_CODES.CONFIG_ERROR;
140
287
  return;
141
288
  }
142
- } else {
143
289
  const result = setConfigKey(config2, key, value);
144
290
  if (result.success) {
145
- const configPath = globalOpts.config ? expandPath(globalOpts.config) : getDefaultConfigPath();
291
+ let configPath;
292
+ if (globalOpts.config) {
293
+ configPath = expandPath(globalOpts.config);
294
+ } else if (scopeResult.scope === "local") {
295
+ configPath = expandPath(getLocalConfigPath());
296
+ } else {
297
+ configPath = expandPath(getDefaultConfigPath());
298
+ }
146
299
  try {
147
- await saveConfig(config2, { path: configPath });
300
+ const existing = await loadTomlFile(configPath);
301
+ const existingValue = getNestedValue(
302
+ config2,
303
+ key
304
+ );
305
+ const parsedValue = parseValue(value, existingValue);
306
+ setNestedValue(existing, key, parsedValue);
307
+ await saveConfig(existing, { path: configPath });
148
308
  if (!globalOpts.quiet) {
149
309
  console.log(`Set ${key} = ${result.value}`);
150
310
  console.log(`Saved to ${configPath}`);
@@ -2204,11 +2364,18 @@ async function main() {
2204
2364
  const currentFile = fileURLToPath(import.meta.url);
2205
2365
  const executedFile = process.argv[1];
2206
2366
  if (executedFile) {
2207
- if (realpathSync(executedFile) === realpathSync(currentFile)) {
2208
- main().catch((error) => {
2209
- console.error("Fatal error:", error);
2210
- process.exit(EXIT_CODES.GENERAL_ERROR);
2211
- });
2367
+ try {
2368
+ if (realpathSync(executedFile) === realpathSync(currentFile)) {
2369
+ main().catch((error) => {
2370
+ console.error("Fatal error:", error);
2371
+ process.exit(EXIT_CODES.GENERAL_ERROR);
2372
+ });
2373
+ }
2374
+ } catch (e) {
2375
+ const code = e instanceof Error ? e.code : void 0;
2376
+ if (code !== "ENOENT" && code !== "ERR_INVALID_ARG_TYPE") {
2377
+ console.error("[debug] Unexpected error resolving entry path:", e);
2378
+ }
2212
2379
  }
2213
2380
  }
2214
2381
  export {