@scemoon/cdh 1.0.0 → 1.0.3

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 (116) hide show
  1. package/cdh/__pycache__/__main__.cpython-314.pyc +0 -0
  2. package/cdh/__pycache__/cli.cpython-314.pyc +0 -0
  3. package/cdh/__pycache__/scaffold.cpython-314.pyc +0 -0
  4. package/cdh/cli.py +473 -87
  5. package/cdh/scaffold.py +386 -121
  6. package/onecode/__pycache__/cli.cpython-314.pyc +0 -0
  7. package/onecode/__pycache__/commands.cpython-314.pyc +0 -0
  8. package/onecode/__pycache__/config.cpython-314.pyc +0 -0
  9. package/onecode/__pycache__/config_screen.cpython-314.pyc +0 -0
  10. package/onecode/agent/__pycache__/cdh_loader.cpython-314.pyc +0 -0
  11. package/onecode/agent/__pycache__/engine.cpython-314.pyc +0 -0
  12. package/onecode/agent/__pycache__/onecode_agent_acp.cpython-314.pyc +0 -0
  13. package/onecode/agent/__pycache__/session.cpython-314.pyc +0 -0
  14. package/onecode/agent/agents/__pycache__/types.cpython-314.pyc +0 -0
  15. package/onecode/agent/agents/types.py +123 -79
  16. package/onecode/agent/cdh_loader.py +26 -13
  17. package/onecode/agent/engine.py +426 -235
  18. package/onecode/agent/onecode_agent_acp.py +200 -73
  19. package/onecode/agent/session.py +8 -13
  20. package/onecode/agent/tools/__pycache__/agent_tools.cpython-314.pyc +0 -0
  21. package/onecode/agent/tools/__pycache__/task_tools.cpython-314.pyc +0 -0
  22. package/onecode/agent/tools/__pycache__/todo_tools.cpython-314.pyc +0 -0
  23. package/onecode/agent/tools/agent_tools.py +4 -4
  24. package/onecode/agent/tools/todo_tools.py +188 -0
  25. package/onecode/cli.py +3 -3
  26. package/onecode/commands.py +439 -194
  27. package/onecode/config.py +3 -3
  28. package/onecode/config_screen.py +4 -2
  29. package/onecode/models/__pycache__/errors.cpython-314.pyc +0 -0
  30. package/onecode/models/__pycache__/messages.cpython-314.pyc +0 -0
  31. package/onecode/models/__pycache__/provider.cpython-314.pyc +0 -0
  32. package/onecode/models/errors.py +11 -1
  33. package/onecode/models/messages.py +35 -16
  34. package/onecode/models/provider.py +243 -12
  35. package/onecode/models/providers/__pycache__/anthropic_provider.cpython-314.pyc +0 -0
  36. package/onecode/models/providers/__pycache__/deepseek_provider.cpython-314.pyc +0 -0
  37. package/onecode/models/providers/__pycache__/glm_provider.cpython-314.pyc +0 -0
  38. package/onecode/models/providers/__pycache__/minimax_provider.cpython-314.pyc +0 -0
  39. package/onecode/models/providers/__pycache__/minimaxi_provider.cpython-314.pyc +0 -0
  40. package/onecode/models/providers/__pycache__/ollama_provider.cpython-314.pyc +0 -0
  41. package/onecode/models/providers/__pycache__/openai_provider.cpython-314.pyc +0 -0
  42. package/onecode/models/providers/anthropic_provider.py +4 -2
  43. package/onecode/models/providers/deepseek_provider.py +2 -0
  44. package/onecode/models/providers/glm_provider.py +2 -1
  45. package/onecode/models/providers/minimax_provider.py +2 -1
  46. package/onecode/models/providers/minimaxi_provider.py +76 -25
  47. package/onecode/models/providers/ollama_provider.py +2 -1
  48. package/onecode/models/providers/openai_provider.py +4 -0
  49. package/onecode/skills/__pycache__/manager.cpython-314.pyc +0 -0
  50. package/onecode/tasks/__pycache__/__init__.cpython-314.pyc +0 -0
  51. package/onecode/tasks/__pycache__/manager.cpython-314.pyc +0 -0
  52. package/onecode/tasks/__pycache__/models.cpython-314.pyc +0 -0
  53. package/onecode/tasks/manager.py +1 -1
  54. package/package.json +1 -1
  55. package/pyproject.toml +10 -0
  56. package/run.js +24 -7
  57. package/tui/__pycache__/app.cpython-314.pyc +0 -0
  58. package/tui/__pycache__/cli.cpython-314.pyc +0 -0
  59. package/tui/__pycache__/db.cpython-314.pyc +0 -0
  60. package/tui/__pycache__/directory_suggester.cpython-314.pyc +0 -0
  61. package/tui/__pycache__/directory_watcher.cpython-314.pyc +0 -0
  62. package/tui/__pycache__/messages.cpython-314.pyc +0 -0
  63. package/tui/__pycache__/paths.cpython-314.pyc +0 -0
  64. package/tui/acp/__pycache__/agent.cpython-314.pyc +0 -0
  65. package/tui/acp/__pycache__/api.cpython-314.pyc +0 -0
  66. package/tui/acp/__pycache__/messages.cpython-314.pyc +0 -0
  67. package/tui/acp/__pycache__/prompt.cpython-314.pyc +0 -0
  68. package/tui/acp/__pycache__/protocol.cpython-314.pyc +0 -0
  69. package/tui/acp/agent.py +60 -11
  70. package/tui/acp/api.py +6 -0
  71. package/tui/acp/messages.py +11 -0
  72. package/tui/acp/prompt.py +1 -1
  73. package/tui/acp/protocol.py +31 -0
  74. package/tui/app.py +60 -8
  75. package/tui/db.py +23 -0
  76. package/tui/directory_watcher.py +35 -1
  77. package/tui/messages.py +4 -1
  78. package/tui/paths.py +3 -3
  79. package/tui/screens/__pycache__/agent_modal.cpython-314.pyc +0 -0
  80. package/tui/screens/__pycache__/component_picker.cpython-314.pyc +0 -0
  81. package/tui/screens/__pycache__/log.cpython-314.pyc +0 -0
  82. package/tui/screens/__pycache__/main.cpython-314.pyc +0 -0
  83. package/tui/screens/__pycache__/projects_app.cpython-314.pyc +0 -0
  84. package/tui/screens/__pycache__/projects_screen.cpython-314.pyc +0 -0
  85. package/tui/screens/__pycache__/settings.cpython-314.pyc +0 -0
  86. package/tui/screens/__pycache__/store.cpython-314.pyc +0 -0
  87. package/tui/screens/component_picker.py +148 -0
  88. package/tui/screens/main.py +47 -95
  89. package/tui/screens/projects.tcss +10 -0
  90. package/tui/screens/projects_app.py +33 -5
  91. package/tui/screens/projects_screen.py +171 -9
  92. package/tui/widgets/__pycache__/acp_content.cpython-314.pyc +0 -0
  93. package/tui/widgets/__pycache__/ask_user.cpython-314.pyc +0 -0
  94. package/tui/widgets/__pycache__/conversation.cpython-314.pyc +0 -0
  95. package/tui/widgets/__pycache__/diff_view.cpython-314.pyc +0 -0
  96. package/tui/widgets/__pycache__/directory_input.cpython-314.pyc +0 -0
  97. package/tui/widgets/__pycache__/mandelbrot.cpython-314.pyc +0 -0
  98. package/tui/widgets/__pycache__/modified_files.cpython-314.pyc +0 -0
  99. package/tui/widgets/__pycache__/plan.cpython-314.pyc +0 -0
  100. package/tui/widgets/__pycache__/project_directory_tree.cpython-314.pyc +0 -0
  101. package/tui/widgets/__pycache__/project_grid_select.cpython-314.pyc +0 -0
  102. package/tui/widgets/__pycache__/question.cpython-314.pyc +0 -0
  103. package/tui/widgets/__pycache__/subagent.cpython-314.pyc +0 -0
  104. package/tui/widgets/__pycache__/subagent_screen.cpython-314.pyc +0 -0
  105. package/tui/widgets/__pycache__/tool_call.cpython-314.pyc +0 -0
  106. package/tui/widgets/ask_user.py +1 -1
  107. package/tui/widgets/conversation.py +99 -42
  108. package/tui/widgets/modified_files.py +155 -23
  109. package/tui/widgets/plan.py +22 -0
  110. package/tui/widgets/project_directory_tree.py +22 -10
  111. package/tui/widgets/project_grid_select.py +23 -1
  112. package/tui/widgets/question.py +1 -1
  113. package/tui/widgets/subagent.py +202 -36
  114. package/tui/widgets/subagent_screen.py +186 -0
  115. package/tui/widgets/tool_call.py +5 -8
  116. package/onecode/agent/tools/task_tools.py +0 -249
package/cdh/cli.py CHANGED
@@ -3,31 +3,38 @@ from typing import Optional
3
3
 
4
4
  import click
5
5
 
6
- from cdh.scaffold import scaffold_dlc_project
6
+ from cdh.scaffold import (
7
+ COMPONENTS,
8
+ COMPONENT_BY_ID,
9
+ CROSS_CUTTING,
10
+ CROSS_CUTTING_BY_ID,
11
+ add_component,
12
+ add_cross_cutting,
13
+ init_dlc_project,
14
+ scaffold_dlc_project,
15
+ )
7
16
  from onecode.cli import cli as onecode_cli
8
17
  from onecode.cli import setup_logging
9
18
  from onecode.config import ensure_dirs, load_config, save_config
10
19
 
11
20
 
12
- _CDH_DIR = Path.home() / ".onecode"
21
+ _CDH_DIR = Path.home() / ".cdh"
13
22
 
14
23
  _COMMON_HELP = """
15
24
  \b
16
25
  Usage:
17
- cdh Launch TUI (agent store)
18
- cdh tui Launch TUI (agent store)
19
- cdh tui --agent cdh.cloud-dev-harness Launch TUI with agent
20
- cdh help Show this help message
21
- cdh config Configuration editor (TUI)
22
- cdh logs View application logs
23
- cdh project Project management
24
- cdh version Show version information
26
+ cdh Launch TUI (agent store)
27
+ cdh tui Launch TUI (agent store)
28
+ cdh onecode <sub> onecode CLI surface (config / codebase / skill / mcp / help)
29
+ cdh project Project management
30
+ cdh session list|load Session management
31
+ cdh version Show version information
25
32
 
26
33
  \b
27
34
  Paths:
28
- Config ~/.onecode/onecode.config.yaml
29
- Logs ~/.onecode/logs/
30
- Projects ~/.onecode/projects/
35
+ Config ~/.cdh/onecode.config.yaml
36
+ Logs ~/.cdh/logs/
37
+ Projects ~/.cdh/projects/
31
38
  """
32
39
 
33
40
 
@@ -45,8 +52,11 @@ def cli(ctx):
45
52
  CDH (Cloud Dev Harness) is an AI agent framework with LLM provider
46
53
  integration, session management, and a Textual-based TUI.
47
54
 
48
- Run without arguments to open the agent store. Use cdh tui --agent
49
- <identity> to launch a specific agent directly.
55
+ Run without arguments to open the agent store, where you can pick
56
+ a launcher agent.
57
+
58
+ All onecode-specific commands live under `cdh onecode <sub>` — e.g.
59
+ `cdh onecode config`, `cdh onecode codebase`, `cdh onecode skill`.
50
60
  """
51
61
  if ctx.invoked_subcommand is None:
52
62
  ensure_dirs()
@@ -56,55 +66,214 @@ def cli(ctx):
56
66
  A2TUIApp().run()
57
67
 
58
68
 
59
- # --- config group ---
69
+ # --- onecode sub-namespace ---
70
+ #
71
+ # `cdh onecode <sub>` exposes the onecode CLI surface under the cdh
72
+ # namespace. The onecode group owns the canonical subcommands
73
+ # (config / codebase / skill / mcp / help); running
74
+ # `cdh onecode config` with no subcommand opens the same interactive
75
+ # TUI editor as before, while `cdh onecode config <sub>` dispatches
76
+ # straight to the onecode implementation.
60
77
 
61
78
  @cli.group(
79
+ "onecode",
62
80
  invoke_without_command=True,
63
- short_help="Open configuration editor",
81
+ short_help="onecode CLI surface (config / codebase / skill / mcp / help)",
64
82
  )
65
83
  @click.pass_context
66
- def config(ctx):
67
- """Open the interactive TUI configuration editor.
68
-
69
- Launches a Textual-based UI for editing all CDH settings
70
- including providers, models, cloud platforms, agent parameters.
71
- """
72
- if ctx.invoked_subcommand is None:
73
- from onecode.config_screen import main as config_main
74
- config_main()
84
+ def onecode_group(ctx):
85
+ """The onecode command surface.
75
86
 
87
+ onecode is the agent framework that powers the TUI. This group
88
+ exposes its full CLI: configuration, codebase indexing, skills,
89
+ MCP servers, and help.
76
90
 
77
- # Reuse subcommands from onecode's config group (mode, model, provider, cloud, log-level, skill, mcp, list)
78
- for _cfg_cmd in onecode_cli.get_command(None, "config").commands.values():
79
- if _cfg_cmd.name not in config.commands and _cfg_cmd.name not in ("tui",):
80
- config.add_command(_cfg_cmd)
91
+ \b
92
+ Sub-commands:
93
+ config Manage onecode configuration (mode, model, provider, log-level, list)
94
+ codebase Build, search, and inspect the onecode codebase index
95
+ skill Install, enable, disable, or remove onecode skills
96
+ mcp Configure MCP servers that onecode connects to at runtime
97
+ help Print the onecode help screen
98
+ version Print the onecode version string
81
99
 
100
+ \b
101
+ Examples:
102
+ cdh onecode config Open the onecode configuration editor
103
+ cdh onecode config model get Show the current default model
104
+ cdh onecode config list Dump the full YAML configuration
105
+ cdh onecode config provider set openai
106
+ cdh onecode codebase index Build the onecode codebase index
107
+ cdh onecode codebase search "auth flow"
108
+ cdh onecode skill list List installed onecode skills
109
+ cdh onecode skill add my-skill Scaffold a new onecode skill
110
+ cdh onecode mcp list List onecode MCP servers
111
+ cdh onecode mcp add my-server https://example.com/mcp
112
+ """
113
+ if ctx.invoked_subcommand is None:
114
+ # No subcommand: show the onecode help screen (matches
115
+ # `onecode --help` so the user can browse what's available).
116
+ click.echo(onecode_cli.get_help(click.Context(onecode_cli)))
117
+
118
+
119
+ # Mount every onecode top-level command (codebase / skill / mcp /
120
+ # help / version) under `cdh onecode <sub>`. We deliberately skip
121
+ # `config` because the onecode CLI's bare `onecode config` only prints
122
+ # `--help`; the user-facing behaviour for `cdh onecode config` is to
123
+ # open the TUI editor, so we install our own config sub-group below.
124
+ _ONECODE_SKIP_AS_CDH_ONECODE = {"tui", "config"}
125
+ for _cmd_name in onecode_cli.commands:
126
+ if _cmd_name in _ONECODE_SKIP_AS_CDH_ONECODE:
127
+ continue
128
+ _cmd = onecode_cli.get_command(None, _cmd_name)
129
+ if _cmd is not None and _cmd_name not in onecode_group.commands:
130
+ onecode_group.add_command(_cmd)
131
+
132
+
133
+ # Override the onecode CLI's CDH-flavoured help text so users see
134
+ # onecode-centric descriptions when they run `cdh onecode --help` or
135
+ # `cdh help onecode`. The onecode CLI itself still ships its own copy
136
+ # (used by `onecode --help`); we don't mutate it.
137
+
138
+ _ONECODE_HELP_OVERRIDES: dict[str, tuple[str, str]] = {
139
+ "config": (
140
+ "Open the onecode configuration editor",
141
+ "Manage onecode configuration: agent mode, default model, LLM "
142
+ "provider, log level, skills, MCP servers. With no subcommand, "
143
+ "launches the interactive Textual editor.",
144
+ ),
145
+ "codebase": (
146
+ "Manage the onecode codebase index",
147
+ "Build and query onecode's local codebase index. Used by the "
148
+ "agent to ground answers in the project's own source files. "
149
+ "Sub-commands: index, reindex, status, search.",
150
+ ),
151
+ "skill": (
152
+ "Manage onecode skills",
153
+ "Install, enable, disable, or remove onecode skills. Skills are "
154
+ "reusable instruction sets the agent loads on demand to extend "
155
+ "its behaviour. Sub-commands: list, add, remove, enable, disable.",
156
+ ),
157
+ "mcp": (
158
+ "Manage onecode MCP servers",
159
+ "Configure Model Context Protocol servers that onecode connects "
160
+ "to at runtime to expose additional tools and resources. "
161
+ "Sub-commands: list, add, remove, enable, disable.",
162
+ ),
163
+ "help": (
164
+ "Show onecode CLI help",
165
+ "Print the onecode CLI help screen with every available command.",
166
+ ),
167
+ "version": (
168
+ "Show onecode version",
169
+ "Print the onecode version string.",
170
+ ),
171
+ }
172
+
173
+ for _cmd_name, (_sh, _doc) in _ONECODE_HELP_OVERRIDES.items():
174
+ _mounted = onecode_group.commands.get(_cmd_name)
175
+ if _mounted is not None:
176
+ _mounted.short_help = _sh
177
+ _mounted.help = _doc
178
+
179
+
180
+ # `cdh onecode config` — wrapper around `onecode config` that opens the
181
+ # interactive TUI editor when invoked with no subcommand, and forwards
182
+ # every subcommand (mode / model / provider / skill / mcp / log-level /
183
+ # list) verbatim to onecode.
184
+
185
+ @onecode_group.group(
186
+ "config",
187
+ invoke_without_command=True,
188
+ short_help="Open the onecode configuration editor (or run `cdh onecode config <sub>`)",
189
+ )
190
+ @click.pass_context
191
+ def onecode_config(ctx):
192
+ """Open the onecode configuration editor.
82
193
 
83
- # --- logs command ---
194
+ With a subcommand, get or set the corresponding onecode setting
195
+ directly from the shell. With no subcommand, launch the interactive
196
+ Textual editor for all onecode settings.
84
197
 
85
- @cli.command(short_help="View application logs")
86
- @click.option("--tail", "-t", default=20, help="Number of recent log lines to show")
87
- @click.option("--follow", "-f", is_flag=True, help="Follow log output")
88
- def logs(tail, follow):
89
- """View CDH application logs.
198
+ \b
199
+ Sub-commands:
200
+ mode Get or set onecode's default agent mode (build / plan / solo)
201
+ model Get or set onecode's default LLM model
202
+ provider Get or set onecode's default LLM provider
203
+ log-level Get or set onecode's root logger verbosity
204
+ skill Manage onecode skills (alias for `cdh onecode skill`)
205
+ mcp Manage onecode MCP servers (alias for `cdh onecode mcp`)
206
+ list Dump the full onecode YAML configuration
90
207
 
91
208
  \b
92
209
  Examples:
93
- cdh logs Show last 20 log lines
94
- cdh logs --tail 100 Show last 100 lines
95
- cdh logs --follow Follow log output
210
+ cdh onecode config Open the interactive editor
211
+ cdh onecode config model get Show the current default model
212
+ cdh onecode config provider set openai
213
+ cdh onecode config list Dump the full YAML configuration
96
214
  """
97
- log_file = _CDH_DIR / "logs" / "cdh.log"
98
- if not log_file.exists():
99
- click.echo("No log file found.")
100
- return
101
- if follow:
102
- click.echo(f"Following {log_file}...")
103
- import subprocess
104
- subprocess.run(["tail", "-f", str(log_file)])
105
- else:
106
- import subprocess
107
- subprocess.run(["tail", "-n", str(tail), str(log_file)])
215
+ if ctx.invoked_subcommand is None:
216
+ from onecode.config_screen import main as config_main
217
+ config_main()
218
+
219
+
220
+ _onecode_config_group = onecode_cli.get_command(None, "config")
221
+ if _onecode_config_group is not None:
222
+ for _cfg_cmd in _onecode_config_group.commands.values():
223
+ if _cfg_cmd.name not in onecode_config.commands and _cfg_cmd.name != "tui":
224
+ onecode_config.add_command(_cfg_cmd)
225
+ # Override the sub-command help to be onecode-centric, not
226
+ # CDH-centric, since users reach these via
227
+ # `cdh onecode config <sub>`.
228
+ _sub_overrides = {
229
+ "mode": (
230
+ "Manage onecode agent mode",
231
+ "Get or set onecode's default agent mode "
232
+ "(build / plan / solo).",
233
+ ),
234
+ "model": (
235
+ "Manage onecode's default LLM model",
236
+ "Get or set the default LLM model onecode uses when "
237
+ "starting a new agent session.",
238
+ ),
239
+ "provider": (
240
+ "Manage onecode's default LLM provider",
241
+ "Get or set the default LLM provider onecode routes "
242
+ "chat-completions requests to.",
243
+ ),
244
+ "log-level": (
245
+ "Manage onecode log level",
246
+ "Get or set onecode's root logger verbosity "
247
+ "(debug / info / warn / error).",
248
+ ),
249
+ "skill": (
250
+ "Manage onecode skills (alias for `cdh onecode skill`)",
251
+ "Same skill-management surface as `cdh onecode skill`.",
252
+ ),
253
+ "mcp": (
254
+ "Manage onecode MCP servers (alias for `cdh onecode mcp`)",
255
+ "Same MCP server surface as `cdh onecode mcp`.",
256
+ ),
257
+ "list": (
258
+ "Show onecode's full YAML configuration",
259
+ "Print every onecode setting currently in effect, "
260
+ "as YAML.",
261
+ ),
262
+ }
263
+ if _cfg_cmd.name in _sub_overrides:
264
+ _sh, _doc = _sub_overrides[_cfg_cmd.name]
265
+ _cfg_cmd.short_help = _sh
266
+ _cfg_cmd.help = _doc
267
+
268
+
269
+ # --- (removed) backward-compatible aliases ---
270
+ #
271
+ # Earlier versions exposed `cdh config` and `cdh codebase` at the top
272
+ # level. Those aliases were removed — use `cdh onecode config` and
273
+ # `cdh onecode codebase` instead. The behaviour is identical:
274
+ # - `cdh onecode config` (no subcommand) opens the same TUI editor.
275
+ # - `cdh onecode config <sub>` is the same as the old `cdh config <sub>`.
276
+ # - `cdh onecode codebase <sub>` is the same as the old `cdh codebase <sub>`.
108
277
 
109
278
 
110
279
  # --- project command ---
@@ -143,11 +312,133 @@ def _interactive_select_project():
143
312
  return None
144
313
 
145
314
 
315
+ def _parse_component_selection(text: str, valid_ids: list[str]) -> list[str]:
316
+ """Parse '1,2,5' / '1-3' / 'all' / 'web,backend' / '' into a list of ids.
317
+
318
+ Accepts either numeric tokens (positional, 1-based) or raw id names.
319
+ Returns an empty list if no valid tokens are found.
320
+ """
321
+ text = text.strip().lower()
322
+ if not text:
323
+ return []
324
+ if text in ("all", "*"):
325
+ return list(valid_ids)
326
+ tokens = [t.strip() for t in text.replace(" ", "").split(",") if t.strip()]
327
+ selected: list[str] = []
328
+ for tok in tokens:
329
+ if tok in valid_ids:
330
+ if tok not in selected:
331
+ selected.append(tok)
332
+ continue
333
+ if "-" in tok:
334
+ try:
335
+ a, b = tok.split("-", 1)
336
+ lo, hi = int(a), int(b)
337
+ except ValueError:
338
+ continue
339
+ if lo > hi:
340
+ lo, hi = hi, lo
341
+ for i in range(lo, hi + 1):
342
+ if 1 <= i <= len(valid_ids) and valid_ids[i - 1] not in selected:
343
+ selected.append(valid_ids[i - 1])
344
+ else:
345
+ try:
346
+ idx = int(tok)
347
+ except ValueError:
348
+ continue
349
+ if 1 <= idx <= len(valid_ids) and valid_ids[idx - 1] not in selected:
350
+ selected.append(valid_ids[idx - 1])
351
+ return selected
352
+
353
+
354
+ def _prompt_components(
355
+ spec_label: str,
356
+ specs: tuple,
357
+ allow_empty: bool = False,
358
+ ) -> list[str]:
359
+ """Interactively prompt the user to choose from a list of specs.
360
+
361
+ Args:
362
+ spec_label: title describing what is being selected
363
+ (e.g. "application components").
364
+ specs: a tuple of ComponentSpec or CrossCutSpec.
365
+ allow_empty: if False, the user must select at least one and
366
+ the prompt will reject empty input.
367
+
368
+ Returns a list of selected spec ids. Raises click.Abort if the user
369
+ fails to provide a valid selection after the retry.
370
+ """
371
+ valid_ids = [s.id for s in specs]
372
+ click.echo(f"Select {spec_label} for the project:")
373
+ for i, s in enumerate(specs, 1):
374
+ click.echo(f" {i}) {s.label:<22s} \u2014 {s.description}")
375
+ if allow_empty:
376
+ hint = "[optional, e.g. 1,2 or 'all' or <Enter> to skip]"
377
+ else:
378
+ hint = "[required, e.g. 1,2 or 'all']"
379
+ click.echo("")
380
+ prompt_text = f"{spec_label.capitalize()} {hint}: "
381
+
382
+ for attempt in range(2):
383
+ raw = click.prompt(prompt_text, default="", show_default=False)
384
+ selected = _parse_component_selection(raw, valid_ids)
385
+ if selected:
386
+ return selected
387
+ if allow_empty and not raw.strip():
388
+ return []
389
+ if attempt == 0:
390
+ if allow_empty:
391
+ click.echo(
392
+ "No valid selection parsed. Try again (or press Enter to skip)."
393
+ )
394
+ else:
395
+ click.echo(
396
+ "At least one selection is required. Try again."
397
+ )
398
+ raise click.Abort()
399
+
400
+
401
+ def _resolve_components_flag(components: Optional[str]) -> list[str]:
402
+ """Resolve a --components flag value to a list of component ids.
403
+
404
+ Empty/None means the caller will run the interactive prompt.
405
+ 'all' expands to every component id.
406
+ """
407
+ if not components:
408
+ return []
409
+ valid_ids = [c.id for c in COMPONENTS]
410
+ selected = _parse_component_selection(components, valid_ids)
411
+ if not selected:
412
+ raise click.BadParameter(
413
+ f"No valid component ids in '{components}'. "
414
+ f"Valid ids: {', '.join(valid_ids)}."
415
+ )
416
+ return selected
417
+
418
+
419
+ def _load_project_record(name: str) -> Optional[dict]:
420
+ """Read a project record from ~/.cdh/projects/{name}.{yaml,yml,json}."""
421
+ import yaml as _yaml
422
+ import json as _json
423
+ projects_dir = _CDH_DIR / "projects"
424
+ for ext in ("yaml", "yml", "json"):
425
+ pf = projects_dir / f"{name}.{ext}"
426
+ if pf.exists():
427
+ text = pf.read_text(encoding="utf-8")
428
+ if ext in ("yaml", "yml"):
429
+ return _yaml.safe_load(text) or {}
430
+ return _json.loads(text)
431
+ return None
432
+
433
+
146
434
  @cli.command(short_help="Project management (TUI)")
147
- @click.argument("action", type=click.Choice(["list", "show", "new", "init", "load", "select"]), default="select")
435
+ @click.argument("action", type=click.Choice(["list", "show", "new", "init", "load", "select", "add-component", "add-cross-cutting"]), default="select")
148
436
  @click.argument("name", required=False)
149
437
  @click.argument("path", required=False, default=".")
150
- def project(action, name, path):
438
+ @click.option("--components", default=None, help="Comma-separated component ids (e.g. 'web,backend') or 'all'. Skips the interactive prompt.")
439
+ @click.option("--component", "component_id", default=None, help="Component id for add-component (e.g. 'native').")
440
+ @click.option("--id", "cross_id", default=None, help="Cross-cutting id for add-cross-cutting (e.g. 'contracts').")
441
+ def project(action, name, path, components, component_id, cross_id):
151
442
  """Manage CDH projects.
152
443
 
153
444
  \b
@@ -156,12 +447,14 @@ def project(action, name, path):
156
447
 
157
448
  \b
158
449
  Actions:
159
- select Open project management TUI (default)
160
- list List all projects
161
- show <name> Show project details
162
- new <name> [path] Create a new project (with ai-dlc-skill scaffold if available)
163
- init [path] Initialize .cdh in an existing directory (no registration)
164
- load <name> Load a project (set as current)
450
+ select Open project management TUI (default)
451
+ list List all projects
452
+ show <name> Show project details
453
+ new <name> [path] Create a new project (interactive component selection)
454
+ init [path] Initialize .cdh in an existing directory (interactive, allows empty)
455
+ load <name> Load a project (set as current)
456
+ add-component <name> Add an application component to an existing project
457
+ add-cross-cutting <name> Add a cross-cutting item to an existing project
165
458
  """
166
459
  import yaml
167
460
  projects_dir = _CDH_DIR / "projects"
@@ -183,7 +476,9 @@ def project(action, name, path):
183
476
  click.echo("Projects:")
184
477
  for pf in project_files:
185
478
  click.echo(f" {pf.stem}")
186
- elif action == "show":
479
+ return
480
+
481
+ if action == "show":
187
482
  if not name:
188
483
  click.echo("Usage: cdh project show <name>")
189
484
  return
@@ -193,31 +488,128 @@ def project(action, name, path):
193
488
  click.echo(pf.read_text())
194
489
  return
195
490
  click.echo(f"Project '{name}' not found.")
196
- elif action == "new":
197
- if not name:
198
- click.echo("Usage: cdh project new <name> [path]")
199
- return
491
+ return
492
+
493
+ if action == "new":
200
494
  from onecode.agent.cdh_loader import CdhProjectLoader
495
+ if not name:
496
+ name = click.prompt("Project name", default="").strip()
497
+ if not name:
498
+ click.echo("Project name is required.")
499
+ raise click.Abort()
201
500
  ws = Path(path).expanduser().resolve()
202
- scaffold_dlc_project(ws, name)
501
+ project_file = projects_dir / f"{name}.yaml"
502
+ if project_file.exists():
503
+ click.echo(f"Error: project '{name}' already exists in the project list.")
504
+ raise click.Abort()
505
+ if components is not None:
506
+ selected_components = _resolve_components_flag(components)
507
+ else:
508
+ selected_components = _prompt_components(
509
+ "application components", COMPONENTS, allow_empty=False
510
+ )
511
+ try:
512
+ scaffold_dlc_project(ws, name, components=selected_components)
513
+ except (ValueError, RuntimeError) as e:
514
+ click.echo(f"Error: {str(e) or type(e).__name__}")
515
+ raise click.Abort()
203
516
  CdhProjectLoader.init_project(ws, name)
204
517
  proj_data = {"name": name, "path": str(ws), "description": ""}
205
- project_file = projects_dir / f"{name}.yaml"
206
518
  project_file.write_text(yaml.dump(proj_data))
207
519
  cfg = load_config()
208
520
  cfg.current_project = name
209
521
  cfg.current_project_path = str(ws)
210
522
  save_config(cfg)
211
- click.echo(f"Created project '{name}' at {ws}")
212
- elif action == "init":
523
+ click.echo(
524
+ f"Created project '{name}' at {ws} "
525
+ f"(components: {', '.join(selected_components)})"
526
+ )
527
+ return
528
+
529
+ if action == "init":
213
530
  from onecode.agent.cdh_loader import CdhProjectLoader
214
- from cdh.scaffold import init_dlc_project
215
531
  target = Path(name or ".").expanduser().resolve()
216
532
  project_name = target.name
217
- init_dlc_project(target, project_name)
533
+ proj_file = projects_dir / f"{project_name}.yaml"
534
+ if proj_file.exists():
535
+ click.echo(f"Error: project '{project_name}' already exists in the project list.")
536
+ raise click.Abort()
537
+ if components is not None:
538
+ selected_components = _resolve_components_flag(components)
539
+ else:
540
+ selected_components = _prompt_components(
541
+ "application components", COMPONENTS, allow_empty=True
542
+ )
543
+ try:
544
+ init_dlc_project(target, project_name)
545
+ except (ValueError, RuntimeError) as e:
546
+ click.echo(f"Error: {str(e) or type(e).__name__}")
547
+ raise click.Abort()
548
+ for cid in selected_components:
549
+ try:
550
+ add_component(target, cid)
551
+ except (ValueError, FileNotFoundError) as e:
552
+ click.echo(f"Error: {str(e) or type(e).__name__}")
218
553
  CdhProjectLoader.init_project(target, project_name)
219
- click.echo(f"Initialized .cdh in {target}")
220
- elif action == "load":
554
+ proj_data = {"name": project_name, "path": str(target), "description": ""}
555
+ proj_file.write_text(yaml.dump(proj_data))
556
+ suffix = (
557
+ f" (components: {', '.join(selected_components)})"
558
+ if selected_components
559
+ else ""
560
+ )
561
+ click.echo(f"Initialized .cdh in {target}{suffix}")
562
+ return
563
+
564
+ if action == "add-component":
565
+ if not name:
566
+ click.echo("Usage: cdh project add-component <name> --component <id>")
567
+ return
568
+ if not component_id:
569
+ click.echo("Usage: cdh project add-component <name> --component <id>")
570
+ click.echo(f"Valid ids: {', '.join(c.id for c in COMPONENTS)}")
571
+ return
572
+ record = _load_project_record(name)
573
+ if record is None:
574
+ click.echo(f"Project '{name}' not found.")
575
+ return
576
+ ws = Path(record.get("path", ".")).expanduser().resolve()
577
+ try:
578
+ added = add_component(ws, component_id)
579
+ except (ValueError, FileNotFoundError) as e:
580
+ click.echo(f"Error: {str(e) or type(e).__name__}")
581
+ raise click.Abort()
582
+ if added:
583
+ click.echo(f"Added component '{component_id}' to '{name}'.")
584
+ else:
585
+ click.echo(f"Component '{component_id}' already present in '{name}'.")
586
+ return
587
+
588
+ if action == "add-cross-cutting":
589
+ if not name:
590
+ click.echo("Usage: cdh project add-cross-cutting <name> --id <id>")
591
+ return
592
+ if not cross_id:
593
+ click.echo("Usage: cdh project add-cross-cutting <name> --id <id>")
594
+ click.echo(f"Valid ids: {', '.join(c.id for c in CROSS_CUTTING)}")
595
+ return
596
+ record = _load_project_record(name)
597
+ if record is None:
598
+ click.echo(f"Project '{name}' not found.")
599
+ return
600
+ ws = Path(record.get("path", ".")).expanduser().resolve()
601
+ try:
602
+ added = add_cross_cutting(ws, cross_id)
603
+ except (ValueError, FileNotFoundError) as e:
604
+ click.echo(f"Error: {str(e) or type(e).__name__}")
605
+ raise click.Abort()
606
+ if added:
607
+ click.echo(f"Added cross-cutting '{cross_id}' to '{name}'.")
608
+ else:
609
+ click.echo(f"Cross-cutting '{cross_id}' already present in '{name}'.")
610
+ return
611
+
612
+ if action == "load":
221
613
  if not name:
222
614
  click.echo("Usage: cdh project load <name>")
223
615
  return
@@ -291,13 +683,12 @@ def session(action, session_id):
291
683
 
292
684
  @cli.command(short_help="Launch the TUI")
293
685
  @click.option("--project-dir", "-d", default=".", help="Project directory")
294
- @click.option("--agent", "agent_identity", default=None, help="Agent identity to auto-launch (e.g. cdh.cloud-dev-harness)")
295
686
  @click.pass_context
296
- def tui(ctx, project_dir, agent_identity):
687
+ def tui(ctx, project_dir):
297
688
  """Launch the CDH TUI (Textual User Interface).
298
689
 
299
690
  By default opens the agent store, showing your configured launcher
300
- agents. Use --agent to skip the store and directly launch an agent.
691
+ agents. Pick an agent from the store to launch it.
301
692
  """
302
693
  ensure_dirs()
303
694
  cfg = load_config()
@@ -306,10 +697,7 @@ def tui(ctx, project_dir, agent_identity):
306
697
  ws = Path(project_dir).expanduser().resolve()
307
698
 
308
699
  from tui.app import A2TUIApp
309
- app = A2TUIApp(
310
- project_dir=str(ws),
311
- launch_agent_identity=agent_identity,
312
- )
700
+ app = A2TUIApp(project_dir=str(ws))
313
701
  app.run()
314
702
 
315
703
 
@@ -322,9 +710,9 @@ def help_cmd(command):
322
710
 
323
711
  \b
324
712
  Examples:
325
- cdh help Show top-level help
326
- cdh help config Show config subcommand help
327
- cdh help logs Show logs command help
713
+ cdh help Show top-level help
714
+ cdh help onecode Show `cdh onecode` sub-commands
715
+ cdh help onecode config Show config sub-commands
328
716
  """
329
717
  if command:
330
718
  cmd = cli.commands.get(command)
@@ -345,12 +733,10 @@ def version():
345
733
  click.echo(f"cdh version {__version__}")
346
734
 
347
735
 
348
- # Attach remaining onecode subcommands selectively
349
- _skip = {"config", "init", "set", "list", "tui", "help", "version", "mcp"}
350
- for cmd_name in onecode_cli.commands:
351
- if cmd_name in _skip:
352
- continue
353
- cli.add_command(onecode_cli.get_command(None, cmd_name))
736
+ # All onecode subcommands are reachable through `cdh onecode <sub>`
737
+ # (see the onecode_group defined above). The cdh top-level command
738
+ # surface is intentionally limited to: tui, onecode, project, session,
739
+ # help, and version.
354
740
 
355
741
 
356
742
  def main():