@drico2008/fincli 0.1.2 → 0.1.9
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 +81 -7
- package/fincli/__init__.py +1 -1
- package/fincli/app/analysis/assistant_context.py +27 -1
- package/fincli/app/analysis/indicators.py +1 -1
- package/fincli/app/analysis/market_structure.py +1 -1
- package/fincli/app/cli/commands.py +12 -4
- package/fincli/app/cli/router.py +253 -13
- package/fincli/app/modules/session_history.py +113 -0
- package/fincli/app/providers/ai/anthropic_provider.py +8 -7
- package/fincli/app/providers/ai/gemini_provider.py +8 -7
- package/fincli/app/providers/ai/groq_provider.py +8 -7
- package/fincli/app/providers/ai/http_provider.py +3 -3
- package/fincli/app/providers/ai/huggingface_provider.py +8 -7
- package/fincli/app/providers/ai/openai_provider.py +8 -7
- package/fincli/app/providers/ai/openrouter_provider.py +8 -7
- package/fincli/app/providers/ai/together_provider.py +8 -7
- package/fincli/app/providers/market/custom_provider.py +2 -2
- package/fincli/app/providers/market/finnhub_provider.py +1 -1
- package/fincli/app/providers/market/manager.py +6 -5
- package/fincli/app/providers/market/news_provider.py +4 -4
- package/fincli/app/providers/market/twelvedata_provider.py +1 -1
- package/fincli/app/providers/market/yfinance_provider.py +1 -1
- package/fincli/app/services/web_research.py +267 -0
- package/fincli/app/storage/cache.py +2 -2
- package/fincli/app/storage/config.py +3 -4
- package/fincli/app/storage/config_paths.py +9 -0
- package/fincli/app/storage/database.py +17 -0
- package/fincli/app/storage/secrets.py +104 -0
- package/fincli/app/tui/components.py +1 -1
- package/fincli/app/tui/layout.py +8 -7
- package/fincli/app/tui/market_provider_selector.py +42 -2
- package/fincli/app/tui/model_selector.py +97 -55
- package/fincli/app/utils/formatting.py +50 -0
- package/npm/bin/fincli.js +9 -2
- package/package.json +1 -1
- package/pyproject.toml +1 -1
|
@@ -15,6 +15,7 @@ from textual.widgets import Input, Static
|
|
|
15
15
|
|
|
16
16
|
from fincli.app.providers.market.manager import MarketProviderManager
|
|
17
17
|
from fincli.app.storage.config import ConfigManager
|
|
18
|
+
from fincli.app.storage.secrets import save_secret
|
|
18
19
|
from fincli.app.utils.formatting import mask_secret
|
|
19
20
|
|
|
20
21
|
|
|
@@ -109,6 +110,7 @@ class MarketProviderSelectorScreen(ModalScreen[tuple[str, ...] | None]):
|
|
|
109
110
|
self.selected_index = 0
|
|
110
111
|
self.selected_provider = config.settings.market_provider
|
|
111
112
|
self.search = ""
|
|
113
|
+
self.pending_env_keys: list[str] = []
|
|
112
114
|
|
|
113
115
|
def compose(self) -> ComposeResult:
|
|
114
116
|
with Vertical(id="ai_selector_card"):
|
|
@@ -131,6 +133,9 @@ class MarketProviderSelectorScreen(ModalScreen[tuple[str, ...] | None]):
|
|
|
131
133
|
|
|
132
134
|
def on_input_submitted(self, event: Input.Submitted) -> None:
|
|
133
135
|
event.stop()
|
|
136
|
+
if self.mode == "key":
|
|
137
|
+
self._save_key(event.value)
|
|
138
|
+
return
|
|
134
139
|
self.action_select()
|
|
135
140
|
|
|
136
141
|
def on_key(self, event: Key) -> None:
|
|
@@ -170,7 +175,11 @@ class MarketProviderSelectorScreen(ModalScreen[tuple[str, ...] | None]):
|
|
|
170
175
|
if self.mode == "provider":
|
|
171
176
|
provider = selected.provider # type: ignore[attr-defined]
|
|
172
177
|
self.selected_provider = provider
|
|
173
|
-
self.
|
|
178
|
+
self.pending_env_keys = [key for key in PROVIDER_ENV_KEYS.get(provider, ()) if not os.getenv(key)]
|
|
179
|
+
if self.pending_env_keys:
|
|
180
|
+
self._set_mode("key")
|
|
181
|
+
else:
|
|
182
|
+
self._set_mode("priority")
|
|
174
183
|
return
|
|
175
184
|
|
|
176
185
|
providers = selected.providers # type: ignore[attr-defined]
|
|
@@ -191,9 +200,16 @@ class MarketProviderSelectorScreen(ModalScreen[tuple[str, ...] | None]):
|
|
|
191
200
|
|
|
192
201
|
def _sync_search_placeholder(self) -> None:
|
|
193
202
|
search = self.query_one("#ai_selector_search", Input)
|
|
194
|
-
search.
|
|
203
|
+
search.password = self.mode == "key"
|
|
204
|
+
if self.mode == "key":
|
|
205
|
+
env_key = self.pending_env_keys[0] if self.pending_env_keys else "API_KEY"
|
|
206
|
+
search.placeholder = f"Paste {env_key}..."
|
|
207
|
+
else:
|
|
208
|
+
search.placeholder = "Search fallback presets..." if self.mode == "priority" else "Search providers..."
|
|
195
209
|
|
|
196
210
|
def _visible_items(self) -> list[MarketProviderChoice] | list[PriorityChoice]:
|
|
211
|
+
if self.mode == "key":
|
|
212
|
+
return []
|
|
197
213
|
if self.mode == "priority":
|
|
198
214
|
presets = list(priority_presets(self.selected_provider))
|
|
199
215
|
if self.search:
|
|
@@ -222,6 +238,10 @@ class MarketProviderSelectorScreen(ModalScreen[tuple[str, ...] | None]):
|
|
|
222
238
|
if self.mode == "priority":
|
|
223
239
|
title.update("Select Provider Priority")
|
|
224
240
|
provider.update(f"[cyan]Primary:[/] {self.selected_provider} [dim](tab to change provider)[/]")
|
|
241
|
+
elif self.mode == "key":
|
|
242
|
+
env_key = self.pending_env_keys[0] if self.pending_env_keys else "API_KEY"
|
|
243
|
+
title.update("Configure Market API Key")
|
|
244
|
+
provider.update(f"[cyan]Provider:[/] {self.selected_provider} [dim]{env_key} saved to ~/.fincli/secrets.env[/]")
|
|
225
245
|
else:
|
|
226
246
|
title.update("Select Market/News Provider")
|
|
227
247
|
provider.update("")
|
|
@@ -231,6 +251,11 @@ class MarketProviderSelectorScreen(ModalScreen[tuple[str, ...] | None]):
|
|
|
231
251
|
|
|
232
252
|
def _items_text(self, items: list[MarketProviderChoice] | list[PriorityChoice]) -> Text:
|
|
233
253
|
text = Text()
|
|
254
|
+
if self.mode == "key":
|
|
255
|
+
env_key = self.pending_env_keys[0] if self.pending_env_keys else "API_KEY"
|
|
256
|
+
text.append(f"Paste {env_key} above, then press Enter.\n", style="bold")
|
|
257
|
+
text.append("The value is stored locally and will not be printed in output.\n", style="dim")
|
|
258
|
+
return text
|
|
234
259
|
text.append("Providers\n" if self.mode == "provider" else "Fallback presets\n", style="bold dim")
|
|
235
260
|
for index, item in enumerate(items):
|
|
236
261
|
selected = index == self.selected_index
|
|
@@ -252,10 +277,25 @@ class MarketProviderSelectorScreen(ModalScreen[tuple[str, ...] | None]):
|
|
|
252
277
|
return text
|
|
253
278
|
|
|
254
279
|
def _help_text(self) -> str:
|
|
280
|
+
if self.mode == "key":
|
|
281
|
+
return "Paste key/value, Enter to save, Esc to close"
|
|
255
282
|
if self.mode == "priority":
|
|
256
283
|
return "Type to search, up/down navigate, Enter to save priority, Tab to change provider, Esc to close"
|
|
257
284
|
return "Type to search, up/down navigate, Enter to select provider, Esc to close"
|
|
258
285
|
|
|
286
|
+
def _save_key(self, value: str) -> None:
|
|
287
|
+
if not self.pending_env_keys or not value.strip():
|
|
288
|
+
return
|
|
289
|
+
env_key = self.pending_env_keys.pop(0)
|
|
290
|
+
save_secret(env_key, value)
|
|
291
|
+
if self.pending_env_keys:
|
|
292
|
+
self._set_mode("key")
|
|
293
|
+
else:
|
|
294
|
+
providers = recommended_provider_priority(self.selected_provider)
|
|
295
|
+
self.config.set_market_provider_priority(list(providers))
|
|
296
|
+
self.on_selected(providers)
|
|
297
|
+
self._set_mode("priority")
|
|
298
|
+
|
|
259
299
|
|
|
260
300
|
def _provider_key_status(choice: MarketProviderChoice) -> str:
|
|
261
301
|
if not choice.env_keys:
|
|
@@ -13,9 +13,10 @@ from textual.events import Key
|
|
|
13
13
|
from textual.screen import ModalScreen
|
|
14
14
|
from textual.widgets import Input, Static
|
|
15
15
|
|
|
16
|
-
from fincli.app.providers.ai.manager import AIProviderManager
|
|
17
|
-
from fincli.app.storage.config import ConfigManager
|
|
18
|
-
from fincli.app.
|
|
16
|
+
from fincli.app.providers.ai.manager import AIProviderManager
|
|
17
|
+
from fincli.app.storage.config import ConfigManager
|
|
18
|
+
from fincli.app.storage.secrets import save_secret
|
|
19
|
+
from fincli.app.utils.formatting import mask_secret
|
|
19
20
|
|
|
20
21
|
|
|
21
22
|
@dataclass(frozen=True, slots=True)
|
|
@@ -248,9 +249,12 @@ class AIModelSelectorScreen(ModalScreen[tuple[str, str] | None]):
|
|
|
248
249
|
self.selected_index = 0
|
|
249
250
|
self._render_selector()
|
|
250
251
|
|
|
251
|
-
def on_input_submitted(self, event: Input.Submitted) -> None:
|
|
252
|
-
event.stop()
|
|
253
|
-
self.
|
|
252
|
+
def on_input_submitted(self, event: Input.Submitted) -> None:
|
|
253
|
+
event.stop()
|
|
254
|
+
if self.mode == "key":
|
|
255
|
+
self._save_key(event.value)
|
|
256
|
+
return
|
|
257
|
+
self.action_select()
|
|
254
258
|
|
|
255
259
|
def on_key(self, event: Key) -> None:
|
|
256
260
|
if event.key == "up":
|
|
@@ -286,20 +290,22 @@ class AIModelSelectorScreen(ModalScreen[tuple[str, str] | None]):
|
|
|
286
290
|
if not items:
|
|
287
291
|
return
|
|
288
292
|
selected = items[self.selected_index]
|
|
289
|
-
if self.mode == "provider":
|
|
290
|
-
provider = selected.provider # type: ignore[attr-defined]
|
|
291
|
-
self.selected_provider = provider
|
|
292
|
-
if _has_key(provider):
|
|
293
|
-
self._set_mode("configured")
|
|
294
|
-
else:
|
|
295
|
-
self._set_mode("
|
|
296
|
-
return
|
|
297
|
-
if self.mode == "configured":
|
|
298
|
-
if selected == "Configure
|
|
299
|
-
self._set_mode("
|
|
300
|
-
|
|
301
|
-
self._set_mode("model")
|
|
302
|
-
|
|
293
|
+
if self.mode == "provider":
|
|
294
|
+
provider = selected.provider # type: ignore[attr-defined]
|
|
295
|
+
self.selected_provider = provider
|
|
296
|
+
if _has_key(provider):
|
|
297
|
+
self._set_mode("configured")
|
|
298
|
+
else:
|
|
299
|
+
self._set_mode("key")
|
|
300
|
+
return
|
|
301
|
+
if self.mode == "configured":
|
|
302
|
+
if selected == "Configure API key":
|
|
303
|
+
self._set_mode("key")
|
|
304
|
+
elif selected == "Change model":
|
|
305
|
+
self._set_mode("model")
|
|
306
|
+
else:
|
|
307
|
+
self._set_mode("model")
|
|
308
|
+
return
|
|
303
309
|
model = selected.model # type: ignore[attr-defined]
|
|
304
310
|
self.config.set_ai_model(self.selected_provider, model)
|
|
305
311
|
self.on_selected(self.selected_provider, model)
|
|
@@ -312,23 +318,31 @@ class AIModelSelectorScreen(ModalScreen[tuple[str, str] | None]):
|
|
|
312
318
|
self.mode = mode
|
|
313
319
|
self.selected_index = 0
|
|
314
320
|
self.search = ""
|
|
315
|
-
self.query_one("#ai_selector_search", Input).value = ""
|
|
316
|
-
self._sync_search_placeholder()
|
|
317
|
-
self._render_selector()
|
|
318
|
-
|
|
319
|
-
def _sync_search_placeholder(self) -> None:
|
|
320
|
-
search = self.query_one("#ai_selector_search", Input)
|
|
321
|
-
search.
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
321
|
+
self.query_one("#ai_selector_search", Input).value = ""
|
|
322
|
+
self._sync_search_placeholder()
|
|
323
|
+
self._render_selector()
|
|
324
|
+
|
|
325
|
+
def _sync_search_placeholder(self) -> None:
|
|
326
|
+
search = self.query_one("#ai_selector_search", Input)
|
|
327
|
+
search.password = self.mode == "key"
|
|
328
|
+
if self.mode == "key":
|
|
329
|
+
choice = _provider_choice(self.selected_provider)
|
|
330
|
+
env_key = choice.env_key if choice else "API_KEY"
|
|
331
|
+
search.placeholder = f"Paste {env_key}..."
|
|
332
|
+
else:
|
|
333
|
+
search.placeholder = "Search models..." if self.mode == "model" else "Search providers..."
|
|
334
|
+
|
|
335
|
+
def _visible_items(self) -> list[ProviderChoice] | list[ModelChoice] | list[str]:
|
|
336
|
+
if self.mode == "provider":
|
|
337
|
+
items = list(PROVIDERS)
|
|
338
|
+
if self.search:
|
|
339
|
+
items = [item for item in items if self.search in item.label.lower() or self.search in item.provider]
|
|
340
|
+
return items
|
|
341
|
+
if self.mode == "configured":
|
|
342
|
+
return ["Use existing configuration", "Configure API key", "Change model"]
|
|
343
|
+
if self.mode == "key":
|
|
344
|
+
return []
|
|
345
|
+
models = list(MODEL_CATALOG.get(self.selected_provider, ()))
|
|
332
346
|
if self.search:
|
|
333
347
|
models = [item for item in models if self.search in item.label.lower() or self.search in item.model.lower()]
|
|
334
348
|
if not models:
|
|
@@ -344,12 +358,17 @@ class AIModelSelectorScreen(ModalScreen[tuple[str, str] | None]):
|
|
|
344
358
|
if self.mode == "provider":
|
|
345
359
|
title.update("Select Provider")
|
|
346
360
|
provider.update("")
|
|
347
|
-
elif self.mode == "configured":
|
|
348
|
-
choice = _provider_choice(self.selected_provider)
|
|
349
|
-
label = choice.label if choice else self.selected_provider
|
|
350
|
-
title.update(f"{label} is already configured")
|
|
351
|
-
provider.update(f"[cyan]Provider:[/] {label} [dim]{_masked_key(self.selected_provider)}[/]")
|
|
352
|
-
|
|
361
|
+
elif self.mode == "configured":
|
|
362
|
+
choice = _provider_choice(self.selected_provider)
|
|
363
|
+
label = choice.label if choice else self.selected_provider
|
|
364
|
+
title.update(f"{label} is already configured")
|
|
365
|
+
provider.update(f"[cyan]Provider:[/] {label} [dim]{_masked_key(self.selected_provider)}[/]")
|
|
366
|
+
elif self.mode == "key":
|
|
367
|
+
choice = _provider_choice(self.selected_provider)
|
|
368
|
+
label = choice.label if choice else self.selected_provider
|
|
369
|
+
title.update("Configure API Key")
|
|
370
|
+
provider.update(f"[cyan]Provider:[/] {label} [dim](saved to ~/.fincli/secrets.env)[/]")
|
|
371
|
+
else:
|
|
353
372
|
choice = _provider_choice(self.selected_provider)
|
|
354
373
|
label = choice.label if choice else self.selected_provider
|
|
355
374
|
title.update("Select Model")
|
|
@@ -361,11 +380,17 @@ class AIModelSelectorScreen(ModalScreen[tuple[str, str] | None]):
|
|
|
361
380
|
|
|
362
381
|
def _items_text(self, items: list[ProviderChoice] | list[ModelChoice] | list[str]) -> Text:
|
|
363
382
|
text = Text()
|
|
364
|
-
if self.mode == "provider":
|
|
365
|
-
text.append("Popular\n", style="bold dim")
|
|
366
|
-
elif self.mode == "model":
|
|
367
|
-
hidden = max(0, len(MODEL_CATALOG.get(self.selected_provider, ())) - len(items))
|
|
368
|
-
text.append(f"{hidden} filtered\n" if self.search else "Available models\n", style="bold dim")
|
|
383
|
+
if self.mode == "provider":
|
|
384
|
+
text.append("Popular\n", style="bold dim")
|
|
385
|
+
elif self.mode == "model":
|
|
386
|
+
hidden = max(0, len(MODEL_CATALOG.get(self.selected_provider, ())) - len(items))
|
|
387
|
+
text.append(f"{hidden} filtered\n" if self.search else "Available models\n", style="bold dim")
|
|
388
|
+
elif self.mode == "key":
|
|
389
|
+
choice = _provider_choice(self.selected_provider)
|
|
390
|
+
env_key = choice.env_key if choice else "API_KEY"
|
|
391
|
+
text.append(f"Paste {env_key} above, then press Enter.\n", style="bold")
|
|
392
|
+
text.append("The key is stored locally and will not be printed in output.\n", style="dim")
|
|
393
|
+
return text
|
|
369
394
|
|
|
370
395
|
for index, item in enumerate(items):
|
|
371
396
|
selected = index == self.selected_index
|
|
@@ -386,12 +411,24 @@ class AIModelSelectorScreen(ModalScreen[tuple[str, str] | None]):
|
|
|
386
411
|
text.append("No matches.\n", style="dim")
|
|
387
412
|
return text
|
|
388
413
|
|
|
389
|
-
def _help_text(self) -> str:
|
|
390
|
-
if self.mode == "
|
|
391
|
-
return "
|
|
414
|
+
def _help_text(self) -> str:
|
|
415
|
+
if self.mode == "key":
|
|
416
|
+
return "Paste API key, Enter to save, Esc to close"
|
|
417
|
+
if self.mode == "configured":
|
|
418
|
+
return "Up/down navigate, Enter to select, Esc to go back"
|
|
392
419
|
if self.mode == "model":
|
|
393
420
|
return "Type to search, up/down navigate, Enter to select, Tab to change provider, Esc to close"
|
|
394
|
-
return "Type to search, up/down navigate, Enter to select, Esc to close"
|
|
421
|
+
return "Type to search, up/down navigate, Enter to select, Esc to close"
|
|
422
|
+
|
|
423
|
+
def _save_key(self, value: str) -> None:
|
|
424
|
+
choice = _provider_choice(self.selected_provider)
|
|
425
|
+
if choice is None or not value.strip():
|
|
426
|
+
return
|
|
427
|
+
save_secret(choice.env_key, value)
|
|
428
|
+
model = self.config.settings.ai_model if self.config.settings.ai_provider == choice.provider else _default_model(choice.provider)
|
|
429
|
+
self.config.set_ai_model(choice.provider, model)
|
|
430
|
+
self.on_selected(choice.provider, model)
|
|
431
|
+
self._set_mode("model")
|
|
395
432
|
|
|
396
433
|
|
|
397
434
|
def _provider_choice(provider: str) -> ProviderChoice | None:
|
|
@@ -404,9 +441,14 @@ def _has_key(provider: str) -> bool:
|
|
|
404
441
|
return bool(choice and os.getenv(choice.env_key))
|
|
405
442
|
|
|
406
443
|
|
|
407
|
-
def _masked_key(provider: str) -> str:
|
|
444
|
+
def _masked_key(provider: str) -> str:
|
|
408
445
|
choice = _provider_choice(provider)
|
|
409
446
|
if choice is None:
|
|
410
447
|
return "not configured"
|
|
411
|
-
masked = mask_secret(os.getenv(choice.env_key))
|
|
412
|
-
return "not configured" if masked == "not set" else f"configured {masked}"
|
|
448
|
+
masked = mask_secret(os.getenv(choice.env_key))
|
|
449
|
+
return "not configured" if masked == "not set" else f"configured {masked}"
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def _default_model(provider: str) -> str:
|
|
453
|
+
info = AIProviderManager().get(provider)
|
|
454
|
+
return info.default_model if info else provider
|
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
from rich.console import Console, ConsoleOptions, RenderResult
|
|
6
|
+
from rich.markdown import Markdown
|
|
7
|
+
from rich.text import Text
|
|
8
|
+
|
|
9
|
+
from fincli.app.providers.ai.base import AIResponse
|
|
10
|
+
|
|
5
11
|
|
|
6
12
|
def mask_secret(value: str | None) -> str:
|
|
7
13
|
"""Mask API keys and tokens before displaying them."""
|
|
@@ -15,3 +21,47 @@ def mask_secret(value: str | None) -> str:
|
|
|
15
21
|
def normalize_symbol(symbol: str) -> str:
|
|
16
22
|
"""Normalize user-entered market symbols."""
|
|
17
23
|
return symbol.strip().upper()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class AIResponseView:
|
|
27
|
+
"""Renderable AI response that preserves Markdown formatting in Rich/Textual."""
|
|
28
|
+
|
|
29
|
+
def __init__(self, response: AIResponse) -> None:
|
|
30
|
+
self.response = response
|
|
31
|
+
|
|
32
|
+
def __str__(self) -> str:
|
|
33
|
+
return (
|
|
34
|
+
f"Provider: {self.response.provider}\n"
|
|
35
|
+
f"Model: {self.response.model}\n"
|
|
36
|
+
f"Response:\n{self.response.content}"
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
|
|
40
|
+
header = Text()
|
|
41
|
+
header.append("Provider: ", style="bold cyan")
|
|
42
|
+
header.append(self.response.provider, style="white")
|
|
43
|
+
header.append(" Model: ", style="bold cyan")
|
|
44
|
+
header.append(self.response.model, style="white")
|
|
45
|
+
yield header
|
|
46
|
+
yield Markdown(self.response.content)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class MarkdownBlock:
|
|
50
|
+
"""Small renderable block for titled Markdown content."""
|
|
51
|
+
|
|
52
|
+
def __init__(self, title: str, body: object, footer: str | None = None) -> None:
|
|
53
|
+
self.title = title
|
|
54
|
+
self.body = body
|
|
55
|
+
self.footer = footer
|
|
56
|
+
|
|
57
|
+
def __str__(self) -> str:
|
|
58
|
+
parts = [self.title, str(self.body)]
|
|
59
|
+
if self.footer:
|
|
60
|
+
parts.append(self.footer)
|
|
61
|
+
return "\n".join(parts)
|
|
62
|
+
|
|
63
|
+
def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
|
|
64
|
+
yield Text(self.title, style="bold cyan")
|
|
65
|
+
yield self.body
|
|
66
|
+
if self.footer:
|
|
67
|
+
yield Text(self.footer, style="dim")
|
package/npm/bin/fincli.js
CHANGED
|
@@ -5,20 +5,27 @@ const path = require("path");
|
|
|
5
5
|
const { spawn } = require("child_process");
|
|
6
6
|
|
|
7
7
|
const packageRoot = path.resolve(__dirname, "..", "..");
|
|
8
|
+
const packageJson = require(path.join(packageRoot, "package.json"));
|
|
8
9
|
const venvDir = path.join(packageRoot, ".npm-python");
|
|
9
10
|
const pythonBin = process.platform === "win32"
|
|
10
11
|
? path.join(venvDir, "Scripts", "python.exe")
|
|
11
12
|
: path.join(venvDir, "bin", "python");
|
|
12
13
|
|
|
13
14
|
function run() {
|
|
15
|
+
const args = process.argv.slice(2);
|
|
16
|
+
if (args.includes("--version") || args.includes("-v")) {
|
|
17
|
+
console.log(packageJson.version);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
14
21
|
if (!fs.existsSync(pythonBin)) {
|
|
15
22
|
console.error("FinCLI Python runtime is missing.");
|
|
16
|
-
console.error("Try reinstalling with: npm install -g fincli");
|
|
23
|
+
console.error("Try reinstalling with: npm install -g @drico2008/fincli");
|
|
17
24
|
console.error("Python 3.11+ must be available during npm install.");
|
|
18
25
|
process.exit(1);
|
|
19
26
|
}
|
|
20
27
|
|
|
21
|
-
const child = spawn(pythonBin, ["-m", "fincli.app.main", ...
|
|
28
|
+
const child = spawn(pythonBin, ["-m", "fincli.app.main", ...args], {
|
|
22
29
|
cwd: packageRoot,
|
|
23
30
|
stdio: "inherit"
|
|
24
31
|
});
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED