npcsh 1.1.21__py3-none-any.whl → 1.1.22__py3-none-any.whl

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 (136) hide show
  1. npcsh/_state.py +10 -5
  2. npcsh/benchmark/npcsh_agent.py +22 -14
  3. npcsh/benchmark/templates/install-npcsh.sh.j2 +2 -2
  4. npcsh/mcp_server.py +9 -1
  5. npcsh/npc_team/alicanto.npc +12 -6
  6. npcsh/npc_team/corca.npc +0 -1
  7. npcsh/npc_team/frederic.npc +2 -3
  8. npcsh/npc_team/jinxs/lib/core/edit_file.jinx +83 -61
  9. npcsh/npc_team/jinxs/modes/alicanto.jinx +102 -41
  10. npcsh/npc_team/jinxs/modes/build.jinx +378 -0
  11. npcsh/npc_team/jinxs/modes/convene.jinx +597 -0
  12. npcsh/npc_team/jinxs/modes/corca.jinx +777 -387
  13. npcsh/npc_team/jinxs/modes/kg.jinx +69 -2
  14. npcsh/npc_team/jinxs/modes/plonk.jinx +16 -7
  15. npcsh/npc_team/jinxs/modes/yap.jinx +628 -187
  16. npcsh/npc_team/kadiefa.npc +2 -1
  17. npcsh/npc_team/sibiji.npc +3 -3
  18. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/alicanto.jinx +102 -41
  19. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/alicanto.npc +12 -6
  20. npcsh-1.1.22.data/data/npcsh/npc_team/build.jinx +378 -0
  21. npcsh-1.1.22.data/data/npcsh/npc_team/corca.jinx +820 -0
  22. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/corca.npc +0 -1
  23. npcsh-1.1.22.data/data/npcsh/npc_team/edit_file.jinx +119 -0
  24. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/frederic.npc +2 -3
  25. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/kadiefa.npc +2 -1
  26. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/kg.jinx +69 -2
  27. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/plonk.jinx +16 -7
  28. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/sibiji.npc +3 -3
  29. npcsh-1.1.22.data/data/npcsh/npc_team/yap.jinx +716 -0
  30. {npcsh-1.1.21.dist-info → npcsh-1.1.22.dist-info}/METADATA +246 -281
  31. {npcsh-1.1.21.dist-info → npcsh-1.1.22.dist-info}/RECORD +127 -130
  32. npcsh/npc_team/jinxs/lib/core/search/kg_search.jinx +0 -429
  33. npcsh/npc_team/jinxs/lib/core/search.jinx +0 -54
  34. npcsh/npc_team/jinxs/lib/utils/build.jinx +0 -65
  35. npcsh-1.1.21.data/data/npcsh/npc_team/build.jinx +0 -65
  36. npcsh-1.1.21.data/data/npcsh/npc_team/corca.jinx +0 -430
  37. npcsh-1.1.21.data/data/npcsh/npc_team/edit_file.jinx +0 -97
  38. npcsh-1.1.21.data/data/npcsh/npc_team/kg_search.jinx +0 -429
  39. npcsh-1.1.21.data/data/npcsh/npc_team/search.jinx +0 -54
  40. npcsh-1.1.21.data/data/npcsh/npc_team/yap.jinx +0 -275
  41. /npcsh/npc_team/jinxs/lib/{core → utils}/chat.jinx +0 -0
  42. /npcsh/npc_team/jinxs/lib/{core → utils}/cmd.jinx +0 -0
  43. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/add_tab.jinx +0 -0
  44. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/alicanto.png +0 -0
  45. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/arxiv.jinx +0 -0
  46. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/benchmark.jinx +0 -0
  47. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/browser_action.jinx +0 -0
  48. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/browser_screenshot.jinx +0 -0
  49. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/chat.jinx +0 -0
  50. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/click.jinx +0 -0
  51. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/close_browser.jinx +0 -0
  52. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/close_pane.jinx +0 -0
  53. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/close_tab.jinx +0 -0
  54. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/cmd.jinx +0 -0
  55. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/compile.jinx +0 -0
  56. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/compress.jinx +0 -0
  57. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/config_tui.jinx +0 -0
  58. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/confirm.jinx +0 -0
  59. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/convene.jinx +0 -0
  60. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/corca.png +0 -0
  61. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/corca_example.png +0 -0
  62. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/db_search.jinx +0 -0
  63. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/delegate.jinx +0 -0
  64. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/file_search.jinx +0 -0
  65. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/focus_pane.jinx +0 -0
  66. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/frederic4.png +0 -0
  67. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/git.jinx +0 -0
  68. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/guac.jinx +0 -0
  69. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/guac.npc +0 -0
  70. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/guac.png +0 -0
  71. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/help.jinx +0 -0
  72. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/incognide.jinx +0 -0
  73. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/init.jinx +0 -0
  74. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/jinxs.jinx +0 -0
  75. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/kadiefa.png +0 -0
  76. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/key_press.jinx +0 -0
  77. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/launch_app.jinx +0 -0
  78. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/list_panes.jinx +0 -0
  79. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/load_file.jinx +0 -0
  80. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/memories.jinx +0 -0
  81. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/models.jinx +0 -0
  82. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/navigate.jinx +0 -0
  83. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/notify.jinx +0 -0
  84. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/npcsh.ctx +0 -0
  85. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/npcsh_sibiji.png +0 -0
  86. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/nql.jinx +0 -0
  87. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/open_browser.jinx +0 -0
  88. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/open_pane.jinx +0 -0
  89. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/ots.jinx +0 -0
  90. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/papers.jinx +0 -0
  91. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/paste.jinx +0 -0
  92. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/plonk.npc +0 -0
  93. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/plonk.png +0 -0
  94. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/plonkjr.png +0 -0
  95. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/pti.jinx +0 -0
  96. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/python.jinx +0 -0
  97. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/read_pane.jinx +0 -0
  98. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/reattach.jinx +0 -0
  99. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/roll.jinx +0 -0
  100. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/run_terminal.jinx +0 -0
  101. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/sample.jinx +0 -0
  102. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/screenshot.jinx +0 -0
  103. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/send_message.jinx +0 -0
  104. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/serve.jinx +0 -0
  105. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/set.jinx +0 -0
  106. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/setup.jinx +0 -0
  107. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/sh.jinx +0 -0
  108. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/shh.jinx +0 -0
  109. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/sibiji.png +0 -0
  110. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/sleep.jinx +0 -0
  111. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/split_pane.jinx +0 -0
  112. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/spool.jinx +0 -0
  113. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/spool.png +0 -0
  114. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/sql.jinx +0 -0
  115. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/switch.jinx +0 -0
  116. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/switch_npc.jinx +0 -0
  117. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/switch_tab.jinx +0 -0
  118. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/switches.jinx +0 -0
  119. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/sync.jinx +0 -0
  120. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/team.jinx +0 -0
  121. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/teamviz.jinx +0 -0
  122. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/trigger.jinx +0 -0
  123. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/type_text.jinx +0 -0
  124. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/usage.jinx +0 -0
  125. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/verbose.jinx +0 -0
  126. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/vixynt.jinx +0 -0
  127. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/wait.jinx +0 -0
  128. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/wander.jinx +0 -0
  129. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/web_search.jinx +0 -0
  130. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/write_file.jinx +0 -0
  131. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/yap.png +0 -0
  132. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/zen_mode.jinx +0 -0
  133. {npcsh-1.1.21.dist-info → npcsh-1.1.22.dist-info}/WHEEL +0 -0
  134. {npcsh-1.1.21.dist-info → npcsh-1.1.22.dist-info}/entry_points.txt +0 -0
  135. {npcsh-1.1.21.dist-info → npcsh-1.1.22.dist-info}/licenses/LICENSE +0 -0
  136. {npcsh-1.1.21.dist-info → npcsh-1.1.22.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,378 @@
1
+ jinx_name: build
2
+ description: Interactive TUI for building deployment artifacts from an NPC team
3
+ interactive: true
4
+ inputs:
5
+ - target: ""
6
+ - outdir: "./build"
7
+ - team: "./npc_team"
8
+ - port: 5337
9
+ - cors: ""
10
+ steps:
11
+ - name: build
12
+ engine: python
13
+ code: |
14
+ import os
15
+ import sys
16
+ import tty
17
+ import termios
18
+ import select as _sel
19
+
20
+ def _resolve_team_path(raw_team):
21
+ """Resolve team path: try given path first, then local ./npc_team, then global ~/.npcsh/npc_team."""
22
+ candidates = []
23
+ if raw_team:
24
+ candidates.append(os.path.abspath(os.path.expanduser(raw_team)))
25
+ local = os.path.abspath('./npc_team')
26
+ global_ = os.path.expanduser('~/.npcsh/npc_team')
27
+ if local not in candidates:
28
+ candidates.append(local)
29
+ if global_ not in candidates:
30
+ candidates.append(global_)
31
+ for p in candidates:
32
+ if os.path.isdir(p):
33
+ if p != candidates[0] and candidates[0] != p:
34
+ print(f"\033[33m⚠ Local team not found at {candidates[0]}, using {p}\033[0m")
35
+ return p
36
+ raise FileNotFoundError(
37
+ f"No npc_team directory found. Searched:\n"
38
+ + "\n".join(f" - {c}" for c in candidates)
39
+ + "\n\nCreate a local npc_team/ or ensure ~/.npcsh/npc_team exists."
40
+ )
41
+
42
+ _direct_target = (context.get('target') or '').strip().lower()
43
+ _direct = bool(_direct_target) or not sys.stdin.isatty()
44
+
45
+ if _direct:
46
+ # Direct build: target passed explicitly or non-interactive
47
+ try:
48
+ from npcpy.build_funcs import (
49
+ build_flask_server as _bf,
50
+ build_docker_compose as _bd,
51
+ build_cli_executable as _bc,
52
+ build_static_site as _bs,
53
+ )
54
+ _target = _direct_target or 'flask'
55
+ _builders = {'flask': _bf, 'docker': _bd, 'cli': _bc, 'static': _bs}
56
+ if _target not in _builders:
57
+ context['output'] = f"Unknown target: {_target}. Available: {list(_builders.keys())}"
58
+ else:
59
+ _cfg = {
60
+ 'team_path': _resolve_team_path(context.get('team')),
61
+ 'output_dir': os.path.abspath(os.path.expanduser(context.get('outdir') or './build')),
62
+ 'target': _target,
63
+ 'port': int(context.get('port') or 5337),
64
+ 'cors_origins': [c.strip() for c in (context.get('cors') or '').split(',') if c.strip()] or None,
65
+ }
66
+ _r = _builders[_target](_cfg)
67
+ context['output'] = _r.get('output', 'Build complete.')
68
+ except ImportError:
69
+ context['output'] = "Build functions not available. Install npcpy with build support."
70
+ except Exception as _e:
71
+ context['output'] = f"Build failed: {_e}"
72
+ context['messages'] = context.get('messages', [])
73
+ exit()
74
+
75
+ try:
76
+ from npcpy.build_funcs import (
77
+ build_flask_server,
78
+ build_docker_compose,
79
+ build_cli_executable,
80
+ build_static_site,
81
+ )
82
+ BUILD_AVAILABLE = True
83
+ except ImportError:
84
+ BUILD_AVAILABLE = False
85
+
86
+ if not BUILD_AVAILABLE:
87
+ context['output'] = "Build functions not available. Install npcpy with build support."
88
+ exit()
89
+
90
+ # ========== State ==========
91
+ class BuildState:
92
+ def __init__(self):
93
+ self.phase = 0 # 0=select target, 1=configure, 2=building, 3=result
94
+ self.sel = 0
95
+ self.targets = [
96
+ {'key': 'flask', 'name': 'Flask Server', 'desc': 'Standalone Python web server with NPC API endpoints'},
97
+ {'key': 'docker', 'name': 'Docker', 'desc': 'Containerized deployment with Dockerfile and docker-compose'},
98
+ {'key': 'cli', 'name': 'CLI Scripts', 'desc': 'Per-NPC executable scripts for direct CLI usage'},
99
+ {'key': 'static', 'name': 'Static Site', 'desc': 'HTML documentation page listing team NPCs'},
100
+ ]
101
+ try:
102
+ _resolved_team = _resolve_team_path(context.get('team'))
103
+ except FileNotFoundError:
104
+ _resolved_team = os.path.expanduser(context.get('team') or './npc_team')
105
+ self.config = {
106
+ 'outdir': os.path.expanduser(context.get('outdir') or './build'),
107
+ 'team': _resolved_team,
108
+ 'port': str(context.get('port') or 5337),
109
+ 'cors': context.get('cors') or '',
110
+ }
111
+ self.config_keys = ['outdir', 'team', 'port', 'cors']
112
+ self.config_labels = {'outdir': 'Output Dir', 'team': 'Team Path', 'port': 'Port', 'cors': 'CORS Origins'}
113
+ self.config_sel = 0
114
+ self.editing = False
115
+ self.edit_buf = ""
116
+ self.edit_key = ""
117
+ self.result = ""
118
+ self.error = ""
119
+ self.files_created = []
120
+
121
+ ui = BuildState()
122
+
123
+ # TUI mode: no target was passed, user picks interactively
124
+
125
+ # ========== Helpers ==========
126
+ def get_size():
127
+ try:
128
+ s = os.get_terminal_size()
129
+ return s.columns, s.lines
130
+ except:
131
+ return 80, 24
132
+
133
+ # ========== Rendering ==========
134
+ def render():
135
+ width, height = get_size()
136
+ out = []
137
+ out.append('\033[2J\033[H')
138
+
139
+ phase_names = ['Select Target', 'Configure', 'Building...', 'Complete']
140
+ header = f' NPC BUILD - {phase_names[ui.phase]} '
141
+ out.append(f'\033[1;1H\033[7;1m{header.ljust(width)}\033[0m')
142
+
143
+ if ui.phase == 0:
144
+ render_targets(out, width, height)
145
+ elif ui.phase == 1:
146
+ render_config(out, width, height)
147
+ elif ui.phase == 2:
148
+ render_building(out, width, height)
149
+ elif ui.phase == 3:
150
+ render_result(out, width, height)
151
+
152
+ if ui.error:
153
+ out.append(f'\033[{height-1};1H\033[K \033[31m{ui.error[:width-3]}\033[0m')
154
+
155
+ sys.stdout.write(''.join(out))
156
+ sys.stdout.flush()
157
+
158
+ def render_targets(out, width, height):
159
+ banner = [
160
+ '\033[33m ██████╗ ██╗ ██╗██╗██╗ ██████╗ \033[0m',
161
+ '\033[33m██╔══██╗██║ ██║██║██║ ██╔══██╗\033[0m',
162
+ '\033[33m██████╔╝██║ ██║██║██║ ██║ ██║\033[0m',
163
+ '\033[33m██╔══██╗██║ ██║██║██║ ██║ ██║\033[0m',
164
+ '\033[33m██████╔╝╚██████╔╝██║███████╗██████╔╝\033[0m',
165
+ '\033[33m╚═════╝ ╚═════╝ ╚═╝╚══════╝╚═════╝ \033[0m',
166
+ ]
167
+ for i, line in enumerate(banner):
168
+ out.append(f'\033[{3+i};3H{line}')
169
+
170
+ y = 3 + len(banner) + 1
171
+ out.append(f'\033[{y};3H\033[1mSelect a build target:\033[0m')
172
+ y += 2
173
+
174
+ for i, target in enumerate(ui.targets):
175
+ selected = (i == ui.sel)
176
+ if selected:
177
+ out.append(f'\033[{y};2H\033[7;1m > {target["name"]:<20}\033[0m \033[7m{target["desc"][:width-28]}\033[0m')
178
+ else:
179
+ out.append(f'\033[{y};2H \033[1m{target["name"]:<20}\033[0m \033[90m{target["desc"][:width-28]}\033[0m')
180
+ y += 2
181
+
182
+ out.append(f'\033[{height};1H\033[K\033[7m j/k:Navigate Enter:Select q:Quit \033[0m'.ljust(width))
183
+
184
+ def render_config(out, width, height):
185
+ target = ui.targets[ui.sel]
186
+ out.append(f'\033[3;3H\033[1mTarget: \033[36m{target["name"]}\033[0m')
187
+ out.append(f'\033[4;3H\033[90m{target["desc"]}\033[0m')
188
+
189
+ out.append(f'\033[6;3H\033[1mConfiguration:\033[0m')
190
+
191
+ y = 8
192
+ for i, key in enumerate(ui.config_keys):
193
+ label = ui.config_labels[key]
194
+ val = ui.config[key]
195
+ selected = (i == ui.config_sel)
196
+
197
+ if ui.editing and key == ui.edit_key:
198
+ out.append(f'\033[{y};3H\033[33m{label}:\033[0m \033[7m {ui.edit_buf}_ \033[0m')
199
+ elif selected:
200
+ out.append(f'\033[{y};3H\033[7m {label}: {val} \033[0m')
201
+ else:
202
+ out.append(f'\033[{y};3H \033[1m{label}:\033[0m {val}')
203
+ y += 2
204
+
205
+ # Show which configs are relevant for this target
206
+ y += 1
207
+ relevant = {'flask': ['outdir', 'team', 'port', 'cors'],
208
+ 'docker': ['outdir', 'team', 'port', 'cors'],
209
+ 'cli': ['outdir', 'team'],
210
+ 'static': ['outdir', 'team']}
211
+ rel = relevant.get(target['key'], ui.config_keys)
212
+ out.append(f'\033[{y};3H\033[90mRelevant for {target["name"]}: {", ".join(rel)}\033[0m')
213
+
214
+ if ui.editing:
215
+ out.append(f'\033[{height};1H\033[K\033[7m Enter:Save Esc:Cancel \033[0m'.ljust(width))
216
+ else:
217
+ out.append(f'\033[{height};1H\033[K\033[7m j/k:Navigate e:Edit Enter:Build Backspace:Back q:Quit \033[0m'.ljust(width))
218
+
219
+ def render_building(out, width, height):
220
+ target = ui.targets[ui.sel]
221
+ mid = height // 2
222
+ out.append(f'\033[{mid};{width//2-10}H\033[33;1mBuilding {target["name"]}...\033[0m')
223
+
224
+ def render_result(out, width, height):
225
+ target = ui.targets[ui.sel]
226
+ y = 3
227
+ if ui.error:
228
+ out.append(f'\033[{y};3H\033[31;1mBuild Failed\033[0m')
229
+ y += 2
230
+ out.append(f'\033[{y};3H\033[31m{ui.error[:width-6]}\033[0m')
231
+ else:
232
+ out.append(f'\033[{y};3H\033[32;1mBuild Complete: {target["name"]}\033[0m')
233
+ y += 2
234
+ for line in ui.result.split('\n'):
235
+ if y >= height - 3:
236
+ break
237
+ out.append(f'\033[{y};3H{line[:width-6]}')
238
+ y += 1
239
+
240
+ out.append(f'\033[{height};1H\033[K\033[7m Enter:New Build o:Open output dir q:Quit \033[0m'.ljust(width))
241
+
242
+ # ========== Build Execution ==========
243
+ def do_build():
244
+ target = ui.targets[ui.sel]
245
+ config = {
246
+ 'team_path': _resolve_team_path(ui.config['team']),
247
+ 'output_dir': os.path.abspath(os.path.expanduser(ui.config['outdir'])),
248
+ 'target': target['key'],
249
+ 'port': int(ui.config['port']),
250
+ 'cors_origins': [c.strip() for c in ui.config['cors'].split(',') if c.strip()] or None,
251
+ }
252
+
253
+ builders = {
254
+ 'flask': build_flask_server,
255
+ 'docker': build_docker_compose,
256
+ 'cli': build_cli_executable,
257
+ 'static': build_static_site,
258
+ }
259
+
260
+ try:
261
+ result = builders[target['key']](config)
262
+ ui.result = result.get('output', 'Build complete.')
263
+ ui.error = ""
264
+ except Exception as e:
265
+ ui.error = str(e)
266
+ ui.result = ""
267
+
268
+ ui.phase = 3
269
+
270
+ # ========== Input ==========
271
+ def handle_input(c, fd):
272
+ if ui.editing:
273
+ return handle_edit(c, fd)
274
+
275
+ if c == '\x1b':
276
+ if _sel.select([fd], [], [], 0.05)[0]:
277
+ c2 = os.read(fd, 1).decode('latin-1')
278
+ if c2 == '[':
279
+ c3 = os.read(fd, 1).decode('latin-1')
280
+ if c3 == 'A': move_up()
281
+ elif c3 == 'B': move_down()
282
+ return True
283
+
284
+ if c == 'q' or c == '\x03':
285
+ return False
286
+
287
+ if ui.phase == 0:
288
+ if c == 'j': move_down()
289
+ elif c == 'k': move_up()
290
+ elif c in ('\r', '\n'):
291
+ ui.phase = 1
292
+ ui.config_sel = 0
293
+ elif ui.phase == 1:
294
+ if c == 'j': move_down()
295
+ elif c == 'k': move_up()
296
+ elif c == 'e':
297
+ key = ui.config_keys[ui.config_sel]
298
+ ui.editing = True
299
+ ui.edit_key = key
300
+ ui.edit_buf = ui.config[key]
301
+ elif c == '\x7f' or c == '\x08':
302
+ ui.phase = 0
303
+ ui.config_sel = 0
304
+ elif c in ('\r', '\n'):
305
+ ui.phase = 2
306
+ render()
307
+ do_build()
308
+ elif ui.phase == 3:
309
+ if c in ('\r', '\n'):
310
+ ui.phase = 0
311
+ ui.sel = 0
312
+ ui.error = ""
313
+ ui.result = ""
314
+ elif c == 'o':
315
+ outdir = os.path.abspath(os.path.expanduser(ui.config['outdir']))
316
+ if os.path.isdir(outdir):
317
+ import subprocess
318
+ try:
319
+ subprocess.Popen(['xdg-open', outdir], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
320
+ except:
321
+ pass
322
+ return True
323
+
324
+ def handle_edit(c, fd):
325
+ if c == '\x1b':
326
+ if _sel.select([fd], [], [], 0.05)[0]:
327
+ os.read(fd, 2)
328
+ ui.editing = False
329
+ ui.edit_buf = ""
330
+ return True
331
+ if c in ('\r', '\n'):
332
+ ui.config[ui.edit_key] = ui.edit_buf
333
+ ui.editing = False
334
+ return True
335
+ if c == '\x7f' or c == '\x08':
336
+ ui.edit_buf = ui.edit_buf[:-1]
337
+ return True
338
+ if c >= ' ' and c <= '~':
339
+ ui.edit_buf += c
340
+ return True
341
+
342
+ def move_up():
343
+ if ui.phase == 0:
344
+ ui.sel = max(0, ui.sel - 1)
345
+ elif ui.phase == 1:
346
+ ui.config_sel = max(0, ui.config_sel - 1)
347
+
348
+ def move_down():
349
+ if ui.phase == 0:
350
+ ui.sel = min(len(ui.targets) - 1, ui.sel + 1)
351
+ elif ui.phase == 1:
352
+ ui.config_sel = min(len(ui.config_keys) - 1, ui.config_sel + 1)
353
+
354
+ # ========== Main Loop ==========
355
+ fd = sys.stdin.fileno()
356
+ old_settings = termios.tcgetattr(fd)
357
+
358
+ try:
359
+ tty.setcbreak(fd)
360
+ sys.stdout.write('\033[?25l')
361
+ render()
362
+
363
+ running = True
364
+ while running:
365
+ if _sel.select([fd], [], [], 0.5)[0]:
366
+ c = os.read(fd, 1).decode('latin-1')
367
+ running = handle_input(c, fd)
368
+ render()
369
+ finally:
370
+ termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
371
+ sys.stdout.write('\033[?25h\033[2J\033[H')
372
+ sys.stdout.flush()
373
+
374
+ if ui.result:
375
+ print(ui.result)
376
+
377
+ context['output'] = ui.result or 'Build cancelled.'
378
+ context['messages'] = context.get('messages', [])