chameleon-cv 0.1.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.
Files changed (151) hide show
  1. package/AGENTS.md +186 -0
  2. package/LICENSE +21 -0
  3. package/Makefile +136 -0
  4. package/README.md +356 -0
  5. package/bin/chameleon.js +136 -0
  6. package/chameleon.bat +286 -0
  7. package/chameleon.sh +353 -0
  8. package/package.json +39 -0
  9. package/profiles/github_repos.json +1266 -0
  10. package/requirements.txt +5 -0
  11. package/scan.sh +12 -0
  12. package/score.sh +12 -0
  13. package/scripts/__init__.py +1 -0
  14. package/scripts/__main_tui__.py +11 -0
  15. package/scripts/__pycache__/__init__.cpython-312.pyc +0 -0
  16. package/scripts/__pycache__/__init__.cpython-313.pyc +0 -0
  17. package/scripts/__pycache__/__main_tui__.cpython-313.pyc +0 -0
  18. package/scripts/__pycache__/ats_ghost.cpython-313.pyc +0 -0
  19. package/scripts/__pycache__/cache.cpython-313.pyc +0 -0
  20. package/scripts/__pycache__/config.cpython-312.pyc +0 -0
  21. package/scripts/__pycache__/config.cpython-313.pyc +0 -0
  22. package/scripts/__pycache__/cover_letter.cpython-313.pyc +0 -0
  23. package/scripts/__pycache__/db.cpython-312.pyc +0 -0
  24. package/scripts/__pycache__/db.cpython-313.pyc +0 -0
  25. package/scripts/__pycache__/fonts.cpython-312.pyc +0 -0
  26. package/scripts/__pycache__/fonts.cpython-313.pyc +0 -0
  27. package/scripts/__pycache__/ghost.cpython-313.pyc +0 -0
  28. package/scripts/__pycache__/job_matcher.cpython-312.pyc +0 -0
  29. package/scripts/__pycache__/job_matcher.cpython-313.pyc +0 -0
  30. package/scripts/__pycache__/question.cpython-313.pyc +0 -0
  31. package/scripts/__pycache__/render.cpython-313.pyc +0 -0
  32. package/scripts/__pycache__/review.cpython-313.pyc +0 -0
  33. package/scripts/__pycache__/setup.cpython-312.pyc +0 -0
  34. package/scripts/__pycache__/tailor_brief.cpython-312.pyc +0 -0
  35. package/scripts/__pycache__/tailor_brief.cpython-313.pyc +0 -0
  36. package/scripts/__pycache__/tailor_cv.cpython-312.pyc +0 -0
  37. package/scripts/__pycache__/tailor_cv.cpython-313.pyc +0 -0
  38. package/scripts/__pycache__/tailor_prompts.cpython-312.pyc +0 -0
  39. package/scripts/__pycache__/tailor_prompts.cpython-313.pyc +0 -0
  40. package/scripts/__pycache__/tui_app.cpython-312.pyc +0 -0
  41. package/scripts/__pycache__/tui_app.cpython-313.pyc +0 -0
  42. package/scripts/ats_ghost.py +170 -0
  43. package/scripts/cache.py +63 -0
  44. package/scripts/cli.py +109 -0
  45. package/scripts/config.py +376 -0
  46. package/scripts/cover_letter.py +105 -0
  47. package/scripts/db.py +331 -0
  48. package/scripts/fonts.py +186 -0
  49. package/scripts/ghost.py +88 -0
  50. package/scripts/job_matcher.py +636 -0
  51. package/scripts/job_scanner/__init__.py +8 -0
  52. package/scripts/job_scanner/__main__.py +6 -0
  53. package/scripts/job_scanner/__pycache__/__init__.cpython-312.pyc +0 -0
  54. package/scripts/job_scanner/__pycache__/__init__.cpython-313.pyc +0 -0
  55. package/scripts/job_scanner/__pycache__/__main__.cpython-312.pyc +0 -0
  56. package/scripts/job_scanner/__pycache__/__main__.cpython-313.pyc +0 -0
  57. package/scripts/job_scanner/__pycache__/base.cpython-312.pyc +0 -0
  58. package/scripts/job_scanner/__pycache__/base.cpython-313.pyc +0 -0
  59. package/scripts/job_scanner/__pycache__/browser.cpython-312.pyc +0 -0
  60. package/scripts/job_scanner/__pycache__/browser.cpython-313.pyc +0 -0
  61. package/scripts/job_scanner/__pycache__/cli.cpython-313.pyc +0 -0
  62. package/scripts/job_scanner/__pycache__/registry.cpython-312.pyc +0 -0
  63. package/scripts/job_scanner/__pycache__/registry.cpython-313.pyc +0 -0
  64. package/scripts/job_scanner/__pycache__/scanner.cpython-312.pyc +0 -0
  65. package/scripts/job_scanner/__pycache__/scanner.cpython-313.pyc +0 -0
  66. package/scripts/job_scanner/base.py +197 -0
  67. package/scripts/job_scanner/browser.py +244 -0
  68. package/scripts/job_scanner/cli.py +6 -0
  69. package/scripts/job_scanner/parsers/__init__.py +24 -0
  70. package/scripts/job_scanner/parsers/__pycache__/__init__.cpython-312.pyc +0 -0
  71. package/scripts/job_scanner/parsers/__pycache__/__init__.cpython-313.pyc +0 -0
  72. package/scripts/job_scanner/parsers/__pycache__/adzuna.cpython-312.pyc +0 -0
  73. package/scripts/job_scanner/parsers/__pycache__/adzuna.cpython-313.pyc +0 -0
  74. package/scripts/job_scanner/parsers/__pycache__/arc.cpython-312.pyc +0 -0
  75. package/scripts/job_scanner/parsers/__pycache__/arc.cpython-313.pyc +0 -0
  76. package/scripts/job_scanner/parsers/__pycache__/crypto_careers.cpython-312.pyc +0 -0
  77. package/scripts/job_scanner/parsers/__pycache__/crypto_careers.cpython-313.pyc +0 -0
  78. package/scripts/job_scanner/parsers/__pycache__/dailyremote.cpython-312.pyc +0 -0
  79. package/scripts/job_scanner/parsers/__pycache__/dailyremote.cpython-313.pyc +0 -0
  80. package/scripts/job_scanner/parsers/__pycache__/flexjobs.cpython-313.pyc +0 -0
  81. package/scripts/job_scanner/parsers/__pycache__/himalayas.cpython-312.pyc +0 -0
  82. package/scripts/job_scanner/parsers/__pycache__/himalayas.cpython-313.pyc +0 -0
  83. package/scripts/job_scanner/parsers/__pycache__/hn_hiring.cpython-312.pyc +0 -0
  84. package/scripts/job_scanner/parsers/__pycache__/hn_hiring.cpython-313.pyc +0 -0
  85. package/scripts/job_scanner/parsers/__pycache__/indeed.cpython-313.pyc +0 -0
  86. package/scripts/job_scanner/parsers/__pycache__/jobicy.cpython-312.pyc +0 -0
  87. package/scripts/job_scanner/parsers/__pycache__/jobicy.cpython-313.pyc +0 -0
  88. package/scripts/job_scanner/parsers/__pycache__/jooble.cpython-312.pyc +0 -0
  89. package/scripts/job_scanner/parsers/__pycache__/jooble.cpython-313.pyc +0 -0
  90. package/scripts/job_scanner/parsers/__pycache__/linkedin.cpython-313.pyc +0 -0
  91. package/scripts/job_scanner/parsers/__pycache__/opentoworkremote.cpython-313.pyc +0 -0
  92. package/scripts/job_scanner/parsers/__pycache__/relocate.cpython-312.pyc +0 -0
  93. package/scripts/job_scanner/parsers/__pycache__/relocate.cpython-313.pyc +0 -0
  94. package/scripts/job_scanner/parsers/__pycache__/remote_rocketship.cpython-313.pyc +0 -0
  95. package/scripts/job_scanner/parsers/__pycache__/remoteok.cpython-312.pyc +0 -0
  96. package/scripts/job_scanner/parsers/__pycache__/remoteok.cpython-313.pyc +0 -0
  97. package/scripts/job_scanner/parsers/__pycache__/remotive.cpython-312.pyc +0 -0
  98. package/scripts/job_scanner/parsers/__pycache__/remotive.cpython-313.pyc +0 -0
  99. package/scripts/job_scanner/parsers/__pycache__/web3_career.cpython-313.pyc +0 -0
  100. package/scripts/job_scanner/parsers/__pycache__/wellfound.cpython-313.pyc +0 -0
  101. package/scripts/job_scanner/parsers/__pycache__/weworkremotely.cpython-312.pyc +0 -0
  102. package/scripts/job_scanner/parsers/__pycache__/weworkremotely.cpython-313.pyc +0 -0
  103. package/scripts/job_scanner/parsers/__pycache__/ycomb.cpython-313.pyc +0 -0
  104. package/scripts/job_scanner/parsers/adzuna.py +81 -0
  105. package/scripts/job_scanner/parsers/arc.py +65 -0
  106. package/scripts/job_scanner/parsers/crypto_careers.py +88 -0
  107. package/scripts/job_scanner/parsers/dailyremote.py +91 -0
  108. package/scripts/job_scanner/parsers/flexjobs.py +91 -0
  109. package/scripts/job_scanner/parsers/himalayas.py +47 -0
  110. package/scripts/job_scanner/parsers/hn_hiring.py +94 -0
  111. package/scripts/job_scanner/parsers/indeed.py +98 -0
  112. package/scripts/job_scanner/parsers/jobicy.py +60 -0
  113. package/scripts/job_scanner/parsers/jooble.py +68 -0
  114. package/scripts/job_scanner/parsers/linkedin.py +140 -0
  115. package/scripts/job_scanner/parsers/opentoworkremote.py +53 -0
  116. package/scripts/job_scanner/parsers/relocate.py +95 -0
  117. package/scripts/job_scanner/parsers/remote_rocketship.py +90 -0
  118. package/scripts/job_scanner/parsers/remoteok.py +63 -0
  119. package/scripts/job_scanner/parsers/remotive.py +57 -0
  120. package/scripts/job_scanner/parsers/web3_career.py +92 -0
  121. package/scripts/job_scanner/parsers/wellfound.py +94 -0
  122. package/scripts/job_scanner/parsers/weworkremotely.py +60 -0
  123. package/scripts/job_scanner/parsers/ycomb.py +83 -0
  124. package/scripts/job_scanner/registry.py +65 -0
  125. package/scripts/job_scanner/scanner.py +162 -0
  126. package/scripts/question.py +107 -0
  127. package/scripts/render.py +110 -0
  128. package/scripts/review.py +89 -0
  129. package/scripts/setup.py +139 -0
  130. package/scripts/tailor_brief.py +379 -0
  131. package/scripts/tailor_cv.py +746 -0
  132. package/scripts/tailor_prompts.py +101 -0
  133. package/scripts/tui/__init__.py +1 -0
  134. package/scripts/tui/__pycache__/__init__.cpython-312.pyc +0 -0
  135. package/scripts/tui/__pycache__/__init__.cpython-313.pyc +0 -0
  136. package/scripts/tui/__pycache__/app.cpython-312.pyc +0 -0
  137. package/scripts/tui/__pycache__/app.cpython-313.pyc +0 -0
  138. package/scripts/tui/__pycache__/helpers.cpython-312.pyc +0 -0
  139. package/scripts/tui/__pycache__/helpers.cpython-313.pyc +0 -0
  140. package/scripts/tui/__pycache__/screens.cpython-312.pyc +0 -0
  141. package/scripts/tui/__pycache__/screens.cpython-313.pyc +0 -0
  142. package/scripts/tui/__pycache__/widgets.cpython-312.pyc +0 -0
  143. package/scripts/tui/__pycache__/widgets.cpython-313.pyc +0 -0
  144. package/scripts/tui/app.py +1547 -0
  145. package/scripts/tui/helpers.py +65 -0
  146. package/scripts/tui/screens.py +642 -0
  147. package/scripts/tui/widgets.py +276 -0
  148. package/scripts/tui_app.py +25 -0
  149. package/scripts/wizard.py +109 -0
  150. package/tui.bat +50 -0
  151. package/tui.sh +59 -0
@@ -0,0 +1,276 @@
1
+ """Reusable widgets: NanoEditor and SettingsPane."""
2
+ from __future__ import annotations
3
+
4
+ from textual import on
5
+ from textual.binding import Binding
6
+ from textual.containers import Horizontal, Vertical, VerticalScroll
7
+ from textual.widgets import Button, Input, Select, Static, TextArea
8
+
9
+ from scripts.config import config
10
+
11
+
12
+ # ═════════════════════════════════════════════════════════════════════════════
13
+ # NANO EDITOR
14
+ # ═════════════════════════════════════════════════════════════════════════════
15
+ class NanoEditor(Vertical):
16
+ DEFAULT_CSS = """
17
+ NanoEditor { height: 1fr; }
18
+ NanoEditor #editor_text { height: 1fr; border: tall $primary; }
19
+ NanoEditor #editor_status { dock: bottom; height: 1; background: $primary; color: $text; padding: 0 1; }
20
+ NanoEditor #editor_hints { dock: bottom; height: 1; background: $secondary-darken-2; color: $text-muted; padding: 0 1; }
21
+ """
22
+ _filename: str | None = None
23
+ _original_text: str = ""
24
+ BINDINGS = [
25
+ Binding("ctrl+x", "editor_save_exit", "Save & Exit", show=True),
26
+ Binding("ctrl+g", "editor_goto", "Go To", show=True),
27
+ ]
28
+
29
+ def compose(self):
30
+ yield TextArea(id="editor_text")
31
+ yield Static(" ^X Save&Exit ^G GoTo", id="editor_hints")
32
+ yield Static(" [bold]New Buffer[/bold] Ln 1, Col 1", id="editor_status")
33
+
34
+ def load_text(self, text: str, filename: str | None = None) -> None:
35
+ try:
36
+ ta = self.query_one("#editor_text", TextArea)
37
+ ta.load_text(text or "")
38
+ self._original_text = text or ""
39
+ if filename:
40
+ self._filename = filename
41
+ self._update_status()
42
+ ta.focus()
43
+ except Exception:
44
+ pass
45
+
46
+ def get_text(self) -> str:
47
+ try:
48
+ return self.query_one("#editor_text", TextArea).text
49
+ except Exception:
50
+ return ""
51
+
52
+ def is_modified(self) -> bool:
53
+ try:
54
+ return self.query_one("#editor_text", TextArea).text != self._original_text
55
+ except Exception:
56
+ return False
57
+
58
+ def _update_status(self) -> None:
59
+ try:
60
+ ta = self.query_one("#editor_text", TextArea)
61
+ loc = ta.cursor_location
62
+ lines = loc[0] + 1
63
+ col = loc[1] + 1
64
+ modified = " [+]" if self.is_modified() else ""
65
+ name = self._filename or "New Buffer"
66
+ self.query_one("#editor_status", Static).update(
67
+ f" [bold]{name}{modified}[/bold]"
68
+ f"{'':>{max(1, 60 - len(name) - len(modified))}}"
69
+ f"Ln {lines}, Col {col}"
70
+ )
71
+ except Exception:
72
+ pass
73
+
74
+ def on_text_area_cursor(self) -> None:
75
+ self._update_status()
76
+
77
+ def on_text_area_changed(self) -> None:
78
+ self._update_status()
79
+
80
+ def action_editor_save_exit(self) -> None:
81
+ from scripts.tui.app import ChameleonTUI
82
+ app = self.app
83
+ if isinstance(app, ChameleonTUI):
84
+ app._save_editor_content()
85
+ try:
86
+ app.query_one("#main_tabs").active = "tab_jobs"
87
+ except Exception:
88
+ pass
89
+
90
+ def action_editor_goto(self) -> None:
91
+ from scripts.tui.screens import GotoLineScreen
92
+ try:
93
+ ta = self.query_one("#editor_text", TextArea)
94
+ self.app.push_screen(GotoLineScreen(ta))
95
+ except Exception:
96
+ pass
97
+
98
+
99
+ # ═════════════════════════════════════════════════════════════════════════════
100
+ # SETTINGS PANE
101
+ # ═════════════════════════════════════════════════════════════════════════════
102
+ class SettingsPane(VerticalScroll):
103
+ """Dynamic settings editor — all config keys editable."""
104
+
105
+ DEFAULT_CSS = """
106
+ SettingsPane { height: 1fr; }
107
+ SettingsPane .section-title {
108
+ background: $primary-darken-1; color: $text;
109
+ padding: 0 1; height: 2; content-align: left middle;
110
+ text-style: bold; margin: 1 0 0 0;
111
+ }
112
+ SettingsPane .setting-row { height: 3; padding: 0 1; layout: horizontal; }
113
+ SettingsPane .setting-label { width: 32; content-align: left middle; color: $text-muted; }
114
+ SettingsPane .setting-value { width: 1fr; }
115
+ SettingsPane #settings_actions { height: 4; padding: 1; layout: horizontal; }
116
+ SettingsPane #settings_actions Button { margin: 0 1; }
117
+ SettingsPane #font_preview {
118
+ height: 3; padding: 0 2;
119
+ background: $surface-darken-1; color: $text;
120
+ content-align: center middle;
121
+ margin: 0 1;
122
+ }
123
+ SettingsPane #settings_status {
124
+ height: 1; padding: 0 1; color: $text-muted;
125
+ text-style: italic;
126
+ }
127
+ """
128
+
129
+ _SECTION_ICONS: dict[str, str] = {
130
+ "Paths": "\U0001f4c1",
131
+ "Scanner": "\U0001f50d",
132
+ "Matching": "\u2694",
133
+ "AI Reviewer": "\U0001f916",
134
+ "Telegram Bot": "\U0001f4e2",
135
+ "WhatsApp": "\U0001f4ac",
136
+ "Bridge/RSS": "\U0001f4e1",
137
+ "Colors": "\U0001f3a8",
138
+ "Fonts": "\u270d",
139
+ "Cache": "\U0001f4be",
140
+ "Rate Limits": "\u23f1",
141
+ "ATS Ghost Text": "\U0001f47b",
142
+ }
143
+
144
+ def compose(self):
145
+ from scripts.fonts import list_available_fonts, get_current_font
146
+
147
+ all_cfg = config.all()
148
+ # Keys already handled by the explicit font selector
149
+ font_handled_keys = {"cv_font_family"}
150
+
151
+ keys_by_section: dict[str, list[str]] = {
152
+ "Paths": [k for k in all_cfg if k.endswith("_path") or k.endswith("_dir")],
153
+ "Scanner": [k for k in all_cfg if k.startswith("scan_")],
154
+ "Matching": [k for k in all_cfg if k.startswith("score_") or k.startswith("lang_") or k.startswith("fw_") or k.startswith("dom_") or k.startswith("domain_")],
155
+ "AI Reviewer": [k for k in all_cfg if k.startswith("opencode_") or k.startswith("model_")],
156
+ "Telegram Bot": [k for k in all_cfg if k.startswith("telegram_")],
157
+ "WhatsApp": [k for k in all_cfg if k.startswith("evolution_") or k.startswith("whatsapp_")],
158
+ "Bridge/RSS": [k for k in all_cfg if k.startswith("rss_") or k.startswith("webhook_")],
159
+ "Colors": [k for k in all_cfg if k.startswith("color_")],
160
+ "Fonts": [k for k in all_cfg if k.startswith("cv_font_") and k not in font_handled_keys],
161
+ "Cache": [k for k in all_cfg if k.startswith("cache_")],
162
+ "Rate Limits": [k for k in all_cfg if k.startswith("rate_") or k.startswith("max_retry") or k.startswith("retry_")],
163
+ "ATS Ghost Text": [k for k in all_cfg if k.startswith("ats_ghost_")],
164
+ }
165
+
166
+ for section, keys in keys_by_section.items():
167
+ if not keys:
168
+ continue
169
+ icon = self._SECTION_ICONS.get(section, "\u25cf")
170
+ yield Static(f" {icon} {section}", classes="section-title")
171
+ for key in keys:
172
+ val = all_cfg[key]
173
+ with Horizontal(classes="setting-row"):
174
+ yield Static(key, classes="setting-label")
175
+ if isinstance(val, bool):
176
+ yield Select(
177
+ [("true", "true"), ("false", "false")],
178
+ value=str(val).lower(),
179
+ id=f"set_{key}",
180
+ classes="setting-value",
181
+ )
182
+ elif isinstance(val, int):
183
+ yield Input(value=str(val), id=f"set_{key}", classes="setting-value", type="integer")
184
+ elif isinstance(val, float):
185
+ yield Input(value=str(val), id=f"set_{key}", classes="setting-value")
186
+ elif isinstance(val, list):
187
+ yield Input(value=",".join(val), id=f"set_{key}", classes="setting-value")
188
+ else:
189
+ yield Input(value=str(val), id=f"set_{key}", classes="setting-value")
190
+
191
+ yield Static(" Font Preview & Selection", classes="section-title")
192
+ yield Static(get_current_font(), id="font_preview")
193
+ fonts = list_available_fonts()
194
+ font_options = [(f["family"], f["family"]) for f in fonts[:50]]
195
+ yield Select(font_options, value=get_current_font(), id="set_cv_font_family", classes="setting-value")
196
+
197
+ with Horizontal(id="settings_actions"):
198
+ yield Button("Save to Disk", id="btn_save_config", variant="primary")
199
+ yield Button("Reload Config", id="btn_reload_config", variant="default")
200
+ yield Button("Reset to Defaults", id="btn_reset_config", variant="error")
201
+ yield Button("Clear Cache", id="btn_clear_cache", variant="default")
202
+ yield Static("[dim]Changes apply in memory instantly. Press Save to persist.[/dim]", id="settings_status")
203
+
204
+ @on(Select.Changed)
205
+ def on_select_changed(self, event: Select.Changed) -> None:
206
+ key = event.select.id
207
+ if key and key.startswith("set_"):
208
+ cfg_key = key[4:]
209
+ val = event.value
210
+ if val == "true":
211
+ config.set(cfg_key, True)
212
+ elif val == "false":
213
+ config.set(cfg_key, False)
214
+ else:
215
+ config.set(cfg_key, val)
216
+ _update_font_preview(self)
217
+
218
+ @on(Input.Changed)
219
+ def on_input_changed(self, event: Input.Changed) -> None:
220
+ key = event.input.id
221
+ if key and key.startswith("set_"):
222
+ cfg_key = key[4:]
223
+ raw = event.value.strip()
224
+ all_cfg = config.all()
225
+ current = all_cfg.get(cfg_key)
226
+ if isinstance(current, bool):
227
+ config.set_nopersist(cfg_key, raw.lower() == "true")
228
+ elif isinstance(current, int):
229
+ try:
230
+ config.set_nopersist(cfg_key, int(raw))
231
+ except (ValueError, TypeError):
232
+ pass
233
+ elif isinstance(current, list):
234
+ config.set_nopersist(cfg_key, [x.strip() for x in raw.split(",") if x.strip()])
235
+ else:
236
+ config.set_nopersist(cfg_key, raw)
237
+ _update_font_preview(self)
238
+
239
+ @on(Button.Pressed, "#btn_save_config")
240
+ def on_save(self) -> None:
241
+ config.persist()
242
+ self.notify("Config saved to disk", timeout=2)
243
+ try:
244
+ self.query_one("#settings_status", Static).update("[green]\u2714 Saved to .chameleon/config.json[/green]")
245
+ except Exception:
246
+ pass
247
+
248
+ @on(Button.Pressed, "#btn_reload_config")
249
+ def on_reload(self) -> None:
250
+ config.reload()
251
+ _update_font_preview(self)
252
+ self.notify("Config reloaded from disk", timeout=2)
253
+
254
+ @on(Button.Pressed, "#btn_reset_config")
255
+ def on_reset(self) -> None:
256
+ from scripts.config import DEFAULTS
257
+ config.set_many(dict(DEFAULTS))
258
+ config.persist()
259
+ _update_font_preview(self)
260
+ self.notify("Config reset to defaults", timeout=2)
261
+
262
+ @on(Button.Pressed, "#btn_clear_cache")
263
+ def on_clear_cache(self) -> None:
264
+ from scripts.cache import cache_clear
265
+ n = cache_clear()
266
+ self.notify(f"Cleared {n} cached items", timeout=2)
267
+
268
+
269
+ def _update_font_preview(pane: SettingsPane) -> None:
270
+ from scripts.fonts import get_current_font
271
+ try:
272
+ st = pane.query_one("#font_preview", Static)
273
+ current = get_current_font()
274
+ st.update(f" Current font: [bold]{current}[/bold] | Sample: The quick brown fox...")
275
+ except Exception:
276
+ pass
@@ -0,0 +1,25 @@
1
+ """Chameleon Job TUI — backward-compatible entry point.
2
+
3
+ All logic now lives in scripts/tui/ package.
4
+ This file exists so `python scripts/tui_app.py` and the ./tui launcher keep working.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import sys
9
+ from pathlib import Path
10
+
11
+ # Ensure project root is on sys.path
12
+ _ROOT = Path(__file__).resolve().parent.parent
13
+ if str(_ROOT) not in sys.path:
14
+ sys.path.insert(0, str(_ROOT))
15
+
16
+ from scripts.tui.app import ChameleonTUI # noqa: E402
17
+
18
+
19
+ def main() -> None:
20
+ app = ChameleonTUI()
21
+ app.run()
22
+
23
+
24
+ if __name__ == "__main__":
25
+ main()
@@ -0,0 +1,109 @@
1
+ """
2
+ Chameleon Interactive Setup Wizard — guided install, config, and first CV.
3
+ """
4
+ from __future__ import annotations
5
+
6
+ import shutil
7
+ import subprocess
8
+ import sys
9
+ from pathlib import Path
10
+
11
+ ROOT = Path(__file__).resolve().parent.parent
12
+ VENV = ROOT / ".venv"
13
+ BIN = VENV / ("Scripts" if sys.platform == "win32" else "bin")
14
+ PYTHON = BIN / ("python.exe" if sys.platform == "win32" else "python3")
15
+
16
+
17
+ def style(text: str, code: str) -> str:
18
+ codes = {"green": "32", "yellow": "33", "cyan": "36", "bold": "1", "dim": "2"}
19
+ parts = ";" .join(codes.get(c, c) for c in code.split())
20
+ return f"\033[{parts}m{text}\033[0m"
21
+
22
+
23
+ def print_header():
24
+ print()
25
+ print(style(" ╔══════════════════════════════════════════╗", "cyan+bold"))
26
+ print(style(" ║ Chameleon Setup Wizard ║", "cyan+bold"))
27
+ print(style(" ║ AI-Powered Resume Tailor v0.1.0 ║", "cyan+bold"))
28
+ print(style(" ╚══════════════════════════════════════════╝", "cyan+bold"))
29
+ print()
30
+
31
+
32
+ def step(num: int, total: int, title: str):
33
+ print(f"\n{style(f'[{num}/{total}]', 'green+bold')} {style(title, 'bold')}")
34
+ print(f" {style('─' * 50, 'dim')}")
35
+
36
+
37
+ def main():
38
+ print_header()
39
+
40
+ from scripts.setup import (
41
+ ensure_venv, install_core, install_rendercv,
42
+ install_playwright, create_dirs, create_config,
43
+ )
44
+
45
+ total = 6
46
+
47
+ step(1, total, "Checking Python")
48
+ try:
49
+ r = subprocess.run([sys.executable, "--version"], capture_output=True, text=True)
50
+ print(f" Found: {r.stdout.strip() or r.stderr.strip()}")
51
+ except FileNotFoundError:
52
+ print(style(" ✗ Python not found! Install Python 3.10+ from https://python.org", "red+bold"))
53
+ sys.exit(1)
54
+
55
+ step(2, total, "Creating Virtual Environment")
56
+ if not ensure_venv(print):
57
+ sys.exit(1)
58
+
59
+ step(3, total, "Installing Dependencies (incl. Playwright + Chromium)")
60
+ install_core(print)
61
+ install_rendercv(print)
62
+ install_playwright(print)
63
+
64
+ step(4, total, "Creating Directories & Config")
65
+ create_dirs(print)
66
+ create_config(print)
67
+
68
+ step(5, total, "First CV Setup")
69
+ cvs = list((ROOT / "templates").glob("*_cv.yaml"))
70
+ if cvs:
71
+ print(f" Found {len(cvs)} existing CV(s):")
72
+ for i, cv in enumerate(cvs, 1):
73
+ print(f" {i}. {cv.name}")
74
+ else:
75
+ print(" No CV found in templates/.")
76
+ src = ROOT / "templates" / "master_template.yaml"
77
+ if src.exists():
78
+ dst = ROOT / "templates" / "my_cv.yaml"
79
+ shutil.copy(src, dst)
80
+ print(style(f" ✓ Created templates/my_cv.yaml from master template", "green"))
81
+ else:
82
+ print(" To add a CV later: chameleon init <path>")
83
+
84
+ step(6, total, "Verify Installation")
85
+ print(" Running tests...")
86
+ r = subprocess.run([str(PYTHON), "-m", "pytest", "tests/", "-q", "--tb=short"], cwd=ROOT,
87
+ capture_output=True, text=True, timeout=60)
88
+ if r.returncode == 0:
89
+ print(style(" ✓ All tests passed", "green"))
90
+ else:
91
+ out = r.stdout.strip() + r.stderr.strip()
92
+ for line in out.split("\n")[-3:]:
93
+ print(f" {line}")
94
+ print(style(" ⚠ Some tests failed (check output above)", "yellow"))
95
+
96
+ print()
97
+ print(style(" ╔══════════════════════════════════════════╗", "green+bold"))
98
+ print(style(" ║ Setup Complete! ║", "green+bold"))
99
+ print(style(" ╚══════════════════════════════════════════╝", "green+bold"))
100
+ print()
101
+ print(f" Next steps:")
102
+ print(f" {style('chameleon tui', 'cyan')} — Launch the terminal UI")
103
+ print(f" {style('chameleon help', 'cyan')} — See all commands")
104
+ print(f" {style('chameleon wizard', 'cyan')} — Re-run this wizard")
105
+ print()
106
+
107
+
108
+ if __name__ == "__main__":
109
+ main()
package/tui.bat ADDED
@@ -0,0 +1,50 @@
1
+ @echo off
2
+ REM Chameleon Job TUI — launcher script (Windows)
3
+ setlocal enabledelayedexpansion
4
+
5
+ set "CHAMELEON_DIR=%~dp0"
6
+ set "VENV_PYTHON=%CHAMELEON_DIR%.venv\Scripts\python.exe"
7
+
8
+ if /I "%1"=="--help" goto :help
9
+ if /I "%1"=="-h" goto :help
10
+ if /I "%1"=="--version" goto :version
11
+
12
+ if not exist "%VENV_PYTHON%" (
13
+ echo [!] Virtual environment not found at %VENV_PYTHON%
14
+ echo Run: make install-tools
15
+ echo Or: python -m venv .venv ^&^& .venv\Scripts\pip install textual pyyaml pypdf reportlab httpx beautifulsoup4 lxml markdownify
16
+ pause
17
+ exit /b 1
18
+ )
19
+
20
+ cd /d "%CHAMELEON_DIR%"
21
+ "%VENV_PYTHON%" -m scripts.tui_app %*
22
+ exit /b %errorlevel%
23
+
24
+ :help
25
+ echo Chameleon Job TUI — scan, score, tailor, and diff
26
+ echo.
27
+ echo Usage: tui.bat [options]
28
+ echo.
29
+ echo Options:
30
+ echo --help, -h Show this help message
31
+ echo --version Show version info
32
+ echo.
33
+ echo Keybindings inside the TUI:
34
+ echo Ctrl+S Scan job platforms
35
+ echo Ctrl+N Paste a job description
36
+ echo Ctrl+E Edit selected job
37
+ echo Ctrl+T Tailor CV for selected job
38
+ echo Ctrl+R Rescore all jobs
39
+ echo Ctrl+D Delete selected job
40
+ echo Ctrl+O Open job URL in browser
41
+ echo / Search jobs
42
+ echo F1 Help screen
43
+ echo Ctrl+Q Quit
44
+ echo Ctrl+1-6 Switch tabs
45
+ echo.
46
+ exit /b 0
47
+
48
+ :version
49
+ echo Chameleon Job TUI v1.0
50
+ exit /b 0
package/tui.sh ADDED
@@ -0,0 +1,59 @@
1
+ #!/bin/bash
2
+ # Chameleon Job TUI — launcher script (Unix/Linux/macOS)
3
+ set -euo pipefail
4
+
5
+ CHAMELEON_DIR="$(cd "$(dirname "$0")" && pwd)"
6
+ VENV_PYTHON="$CHAMELEON_DIR/.venv/bin/python3"
7
+
8
+ show_help() {
9
+ cat <<EOF
10
+ Chameleon Job TUI — scan, score, tailor, and diff
11
+
12
+ Usage: ./tui.sh [options]
13
+
14
+ Options:
15
+ --help, -h Show this help message
16
+ --version Show version info
17
+
18
+ Keybindings inside the TUI:
19
+ Ctrl+S Scan job platforms
20
+ Ctrl+N Paste a job description
21
+ Ctrl+E Edit selected job
22
+ Ctrl+T Tailor CV for selected job
23
+ Ctrl+R Rescore all jobs
24
+ Ctrl+D Delete selected job
25
+ Ctrl+O Open job URL in browser
26
+ / Search jobs
27
+ F1 Help screen
28
+ Ctrl+Q Quit
29
+ Ctrl+1-6 Switch tabs
30
+
31
+ EOF
32
+ exit 0
33
+ }
34
+
35
+ if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then
36
+ show_help
37
+ fi
38
+
39
+ if [[ "${1:-}" == "--version" ]]; then
40
+ echo "Chameleon Job TUI v1.0"
41
+ exit 0
42
+ fi
43
+
44
+ # Ensure venv exists
45
+ if [[ ! -f "$VENV_PYTHON" ]]; then
46
+ echo "[!] Virtual environment not found at $VENV_PYTHON"
47
+ echo " Run: ./chameleon setup"
48
+ echo " Or: python3 -m venv .venv && .venv/bin/pip install textual pyyaml httpx reportlab beautifulsoup4 lxml markdownify"
49
+ exit 1
50
+ fi
51
+
52
+ # Quick dep check
53
+ $VENV_PYTHON -c "import textual" 2>/dev/null || {
54
+ echo "[!] textual not installed. Run: $VENV_PYTHON -m pip install textual pyyaml httpx"
55
+ exit 1
56
+ }
57
+
58
+ cd "$CHAMELEON_DIR" || exit 1
59
+ exec "$VENV_PYTHON" -m scripts.tui_app "$@"