@scemoon/cdh 1.0.0 → 1.0.4

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 (147) hide show
  1. package/cdh/__pycache__/cli.cpython-314.pyc +0 -0
  2. package/cdh/__pycache__/scaffold.cpython-314.pyc +0 -0
  3. package/cdh/cli.py +473 -87
  4. package/cdh/scaffold.py +386 -121
  5. package/onecode/__pycache__/cli.cpython-314.pyc +0 -0
  6. package/onecode/__pycache__/commands.cpython-314.pyc +0 -0
  7. package/onecode/__pycache__/config.cpython-314.pyc +0 -0
  8. package/onecode/agent/__pycache__/cdh_loader.cpython-314.pyc +0 -0
  9. package/onecode/agent/__pycache__/engine.cpython-314.pyc +0 -0
  10. package/onecode/agent/__pycache__/onecode_agent_acp.cpython-314.pyc +0 -0
  11. package/onecode/agent/__pycache__/session.cpython-314.pyc +0 -0
  12. package/onecode/agent/agents/__pycache__/types.cpython-314.pyc +0 -0
  13. package/onecode/agent/agents/types.py +123 -79
  14. package/onecode/agent/cdh_loader.py +26 -13
  15. package/onecode/agent/engine.py +426 -235
  16. package/onecode/agent/onecode_agent_acp.py +202 -75
  17. package/onecode/agent/session.py +8 -13
  18. package/onecode/agent/tools/__pycache__/agent_tools.cpython-314.pyc +0 -0
  19. package/onecode/agent/tools/__pycache__/todo_tools.cpython-314.pyc +0 -0
  20. package/onecode/agent/tools/agent_tools.py +4 -4
  21. package/onecode/agent/tools/todo_tools.py +188 -0
  22. package/onecode/cli.py +3 -3
  23. package/onecode/commands.py +439 -194
  24. package/onecode/config.py +3 -3
  25. package/onecode/config_screen.py +4 -2
  26. package/onecode/models/__pycache__/errors.cpython-314.pyc +0 -0
  27. package/onecode/models/__pycache__/messages.cpython-314.pyc +0 -0
  28. package/onecode/models/__pycache__/provider.cpython-314.pyc +0 -0
  29. package/onecode/models/errors.py +11 -1
  30. package/onecode/models/messages.py +35 -16
  31. package/onecode/models/provider.py +243 -12
  32. package/onecode/models/providers/__pycache__/anthropic_provider.cpython-314.pyc +0 -0
  33. package/onecode/models/providers/__pycache__/deepseek_provider.cpython-314.pyc +0 -0
  34. package/onecode/models/providers/__pycache__/glm_provider.cpython-314.pyc +0 -0
  35. package/onecode/models/providers/__pycache__/minimax_provider.cpython-314.pyc +0 -0
  36. package/onecode/models/providers/__pycache__/minimaxi_provider.cpython-314.pyc +0 -0
  37. package/onecode/models/providers/__pycache__/ollama_provider.cpython-314.pyc +0 -0
  38. package/onecode/models/providers/__pycache__/openai_provider.cpython-314.pyc +0 -0
  39. package/onecode/models/providers/anthropic_provider.py +4 -2
  40. package/onecode/models/providers/deepseek_provider.py +2 -0
  41. package/onecode/models/providers/glm_provider.py +2 -1
  42. package/onecode/models/providers/minimax_provider.py +2 -1
  43. package/onecode/models/providers/minimaxi_provider.py +76 -25
  44. package/onecode/models/providers/ollama_provider.py +2 -1
  45. package/onecode/models/providers/openai_provider.py +4 -0
  46. package/onecode/tasks/manager.py +1 -1
  47. package/package.json +1 -1
  48. package/pyproject.toml +10 -0
  49. package/run.js +24 -7
  50. package/tui/__pycache__/agents.cpython-314.pyc +0 -0
  51. package/tui/__pycache__/app.cpython-314.pyc +0 -0
  52. package/tui/__pycache__/db.cpython-314.pyc +0 -0
  53. package/tui/__pycache__/directory_watcher.cpython-314.pyc +0 -0
  54. package/tui/__pycache__/messages.cpython-314.pyc +0 -0
  55. package/tui/__pycache__/paths.cpython-314.pyc +0 -0
  56. package/tui/__pycache__/settings_schema.cpython-314.pyc +0 -0
  57. package/tui/acp/__pycache__/agent.cpython-314.pyc +0 -0
  58. package/tui/acp/__pycache__/api.cpython-314.pyc +0 -0
  59. package/tui/acp/__pycache__/messages.cpython-314.pyc +0 -0
  60. package/tui/acp/__pycache__/prompt.cpython-314.pyc +0 -0
  61. package/tui/acp/__pycache__/protocol.cpython-314.pyc +0 -0
  62. package/tui/acp/agent.py +60 -11
  63. package/tui/acp/api.py +6 -0
  64. package/tui/acp/messages.py +11 -0
  65. package/tui/acp/prompt.py +1 -1
  66. package/tui/acp/protocol.py +31 -0
  67. package/tui/agents.py +8 -7
  68. package/tui/app.py +60 -8
  69. package/tui/data/agents/ampcode.com.toml +1 -0
  70. package/tui/data/agents/augmentcode.com.toml +1 -0
  71. package/tui/data/agents/cdh.cloud-dev-harness.toml +3 -2
  72. package/tui/data/agents/crowai.dev.toml +1 -0
  73. package/tui/data/agents/deepagents.langchain.com +1 -0
  74. package/tui/data/agents/docker.com.toml +1 -0
  75. package/tui/data/agents/goose.ai.toml +1 -0
  76. package/tui/data/agents/inference.huggingface.co.toml +1 -1
  77. package/tui/data/agents/kiro.dev.toml +1 -0
  78. package/tui/data/agents/onecode.dev.toml +22 -0
  79. package/tui/data/agents/openclaw.ai.toml +1 -1
  80. package/tui/data/agents/openhands.dev.toml +1 -0
  81. package/tui/data/agents/stakpak.dev.toml +1 -0
  82. package/tui/data/agents/vibe.mistral.ai.toml +1 -0
  83. package/tui/data/agents/vtcode.dev.toml +1 -0
  84. package/tui/db.py +23 -0
  85. package/tui/directory_watcher.py +35 -1
  86. package/tui/messages.py +4 -1
  87. package/tui/paths.py +3 -3
  88. package/tui/screens/__pycache__/main.cpython-314.pyc +0 -0
  89. package/tui/screens/component_picker.py +148 -0
  90. package/tui/screens/main.py +47 -95
  91. package/tui/screens/projects.tcss +10 -0
  92. package/tui/screens/projects_app.py +33 -5
  93. package/tui/screens/projects_screen.py +171 -9
  94. package/tui/screens/store.py +1 -1
  95. package/tui/settings_schema.py +1 -1
  96. package/tui/widgets/__pycache__/ask_user.cpython-314.pyc +0 -0
  97. package/tui/widgets/__pycache__/conversation.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__/question.cpython-314.pyc +0 -0
  102. package/tui/widgets/__pycache__/subagent.cpython-314.pyc +0 -0
  103. package/tui/widgets/__pycache__/tool_call.cpython-314.pyc +0 -0
  104. package/tui/widgets/ask_user.py +1 -1
  105. package/tui/widgets/conversation.py +99 -42
  106. package/tui/widgets/modified_files.py +191 -65
  107. package/tui/widgets/plan.py +22 -0
  108. package/tui/widgets/project_directory_tree.py +22 -10
  109. package/tui/widgets/project_grid_select.py +23 -1
  110. package/tui/widgets/question.py +1 -1
  111. package/tui/widgets/subagent.py +202 -36
  112. package/tui/widgets/subagent_screen.py +186 -0
  113. package/tui/widgets/tool_call.py +5 -8
  114. package/cdh/__pycache__/__init__.cpython-313.pyc +0 -0
  115. package/cdh/__pycache__/scaffold.cpython-313.pyc +0 -0
  116. package/onecode/__pycache__/config_screen.cpython-314.pyc +0 -0
  117. package/onecode/__pycache__/mock_data.cpython-314.pyc +0 -0
  118. package/onecode/agent/__pycache__/cdh_agent_acp.cpython-313.pyc +0 -0
  119. package/onecode/agent/__pycache__/cdh_agent_acp.cpython-314.pyc +0 -0
  120. package/onecode/agent/__pycache__/harness_skill.cpython-314.pyc +0 -0
  121. package/onecode/agent/tools/__pycache__/task_tools.cpython-314.pyc +0 -0
  122. package/onecode/agent/tools/task_tools.py +0 -249
  123. package/onecode/codebase/__pycache__/config.cpython-314.pyc +0 -0
  124. package/onecode/models/providers/__pycache__/mock_provider.cpython-314.pyc +0 -0
  125. package/onecode/skills/__pycache__/manager.cpython-314.pyc +0 -0
  126. package/tui/__pycache__/danger.cpython-314.pyc +0 -0
  127. package/tui/__pycache__/directory_suggester.cpython-314.pyc +0 -0
  128. package/tui/screens/__pycache__/log.cpython-314.pyc +0 -0
  129. package/tui/screens/__pycache__/projects_screen.cpython-314.pyc +0 -0
  130. package/tui/screens/__pycache__/sessions.cpython-314.pyc +0 -0
  131. package/tui/screens/__pycache__/settings.cpython-314.pyc +0 -0
  132. package/tui/screens/__pycache__/store.cpython-314.pyc +0 -0
  133. package/tui/widgets/__pycache__/acp_content.cpython-314.pyc +0 -0
  134. package/tui/widgets/__pycache__/conversation.cpython-313.pyc +0 -0
  135. package/tui/widgets/__pycache__/diff_view.cpython-314.pyc +0 -0
  136. package/tui/widgets/__pycache__/directory_input.cpython-314.pyc +0 -0
  137. package/tui/widgets/__pycache__/future_text.cpython-314.pyc +0 -0
  138. package/tui/widgets/__pycache__/grid_select.cpython-314.pyc +0 -0
  139. package/tui/widgets/__pycache__/mandelbrot.cpython-314.pyc +0 -0
  140. package/tui/widgets/__pycache__/markdown_note.cpython-314.pyc +0 -0
  141. package/tui/widgets/__pycache__/project_grid_select.cpython-314.pyc +0 -0
  142. package/tui/widgets/__pycache__/project_summary.cpython-314.pyc +0 -0
  143. package/tui/widgets/__pycache__/session_grid_select.cpython-314.pyc +0 -0
  144. package/tui/widgets/__pycache__/session_summary.cpython-314.pyc +0 -0
  145. package/tui/widgets/__pycache__/shell_result.cpython-314.pyc +0 -0
  146. package/tui/widgets/__pycache__/terminal.cpython-313.pyc +0 -0
  147. package/tui/widgets/__pycache__/tool_call.cpython-313.pyc +0 -0
package/cdh/scaffold.py CHANGED
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from dataclasses import dataclass, field
3
4
  from pathlib import Path
4
5
  from typing import Optional
5
6
 
@@ -8,15 +9,152 @@ import yaml
8
9
 
9
10
  SKILL_NAME = "ai-dlc-skill"
10
11
 
11
- SKILL_YAML_COMPONENTS = [
12
- {"id": "native", "kind": "mobile", "tech": "react-native | flutter", "default_language": "dart", "default_ui_framework": "flutter-sdk", "owns": "apps/native", "fr_prefix": "NATIVE"},
13
- {"id": "desktop", "kind": "desktop", "tech": "electron | tauri", "default_language": "typescript", "default_ui_framework": "electron-react", "owns": "apps/desktop", "fr_prefix": "DESKTOP"},
14
- {"id": "web", "kind": "frontend", "tech": "react | vue | svelte", "default_language": "typescript", "default_ui_framework": "nextjs", "owns": "apps/web", "fr_prefix": "WEB"},
15
- {"id": "backend", "kind": "service", "tech": "python | node | go", "owns": "apps/backend", "fr_prefix": "BE"},
16
- {"id": "wxa", "kind": "mini-program", "tech": "miniprogram", "default_language": "javascript", "default_ui_framework": "vant-weapp", "owns": "apps/wxa", "fr_prefix": "WXA"},
17
- {"id": "mya", "kind": "mini-program", "tech": "miniprogram", "default_language": "javascript", "default_ui_framework": "ant-design-mini", "owns": "apps/mya", "fr_prefix": "MYA"},
18
- {"id": "tta", "kind": "mini-program", "tech": "miniprogram", "default_language": "typescript", "owns": "apps/tta", "fr_prefix": "TTA"},
19
- ]
12
+
13
+ @dataclass(frozen=True)
14
+ class ComponentSpec:
15
+ id: str
16
+ kind: str
17
+ owns: str
18
+ fr_prefix: str
19
+ tech: str
20
+ label: str
21
+ description: str
22
+
23
+
24
+ @dataclass(frozen=True)
25
+ class CrossCutSpec:
26
+ id: str
27
+ paths: tuple[str, ...]
28
+ label: str
29
+ description: str
30
+
31
+
32
+ COMPONENTS: tuple[ComponentSpec, ...] = (
33
+ ComponentSpec(
34
+ id="native",
35
+ kind="mobile",
36
+ owns="apps/native",
37
+ fr_prefix="NATIVE",
38
+ tech="react-native | flutter",
39
+ label="Mobile App",
40
+ description="iOS/Android cross-platform (RN/Flutter)",
41
+ ),
42
+ ComponentSpec(
43
+ id="desktop",
44
+ kind="desktop",
45
+ owns="apps/desktop",
46
+ fr_prefix="DESKTOP",
47
+ tech="electron | tauri",
48
+ label="Desktop App",
49
+ description="Cross-platform desktop (Electron/Tauri)",
50
+ ),
51
+ ComponentSpec(
52
+ id="web",
53
+ kind="frontend",
54
+ owns="apps/web",
55
+ fr_prefix="WEB",
56
+ tech="react | vue | svelte",
57
+ label="Web Frontend",
58
+ description="Browser SPA/SSR (React/Vue/Svelte)",
59
+ ),
60
+ ComponentSpec(
61
+ id="backend",
62
+ kind="service",
63
+ owns="apps/backend",
64
+ fr_prefix="BE",
65
+ tech="python | node | go",
66
+ label="Backend Service",
67
+ description="HTTP API/microservice (Python/Node/Go)",
68
+ ),
69
+ ComponentSpec(
70
+ id="wxa",
71
+ kind="mini-program",
72
+ owns="apps/wxa",
73
+ fr_prefix="WXA",
74
+ tech="miniprogram",
75
+ label="WeChat Mini-Program",
76
+ description="WeChat ecosystem mini-program",
77
+ ),
78
+ ComponentSpec(
79
+ id="mya",
80
+ kind="mini-program",
81
+ owns="apps/mya",
82
+ fr_prefix="MYA",
83
+ tech="miniprogram",
84
+ label="Alipay Mini-Program",
85
+ description="Alipay ecosystem mini-program",
86
+ ),
87
+ ComponentSpec(
88
+ id="tta",
89
+ kind="mini-program",
90
+ owns="apps/tta",
91
+ fr_prefix="TTA",
92
+ tech="miniprogram",
93
+ label="TikTok Mini-Program",
94
+ description="TikTok/Douyin ecosystem mini-program",
95
+ ),
96
+ )
97
+
98
+
99
+ COMPONENT_BY_ID: dict[str, ComponentSpec] = {c.id: c for c in COMPONENTS}
100
+
101
+
102
+ CROSS_CUTTING: tuple[CrossCutSpec, ...] = (
103
+ CrossCutSpec(
104
+ id="contracts",
105
+ paths=("contracts/api", "contracts/events", "contracts/CHANGELOG.md"),
106
+ label="Interface Contracts",
107
+ description="API/event contracts and changelog",
108
+ ),
109
+ CrossCutSpec(
110
+ id="shared",
111
+ paths=("packages/shared",),
112
+ label="Shared Types",
113
+ description="Cross-component shared types package",
114
+ ),
115
+ CrossCutSpec(
116
+ id="openspec",
117
+ paths=("openspec/changes",),
118
+ label="OpenSpec Changes",
119
+ description="OpenSpec change proposals",
120
+ ),
121
+ CrossCutSpec(
122
+ id="cross_stack_features",
123
+ paths=("features/cross-stack",),
124
+ label="Cross-Stack BDD",
125
+ description="End-to-end BDD features across components",
126
+ ),
127
+ CrossCutSpec(
128
+ id="cross_stack_tests",
129
+ paths=("tests/contract", "tests/cross-stack"),
130
+ label="Cross-Stack Tests",
131
+ description="Contract tests and cross-stack integration tests",
132
+ ),
133
+ CrossCutSpec(
134
+ id="provider",
135
+ paths=(
136
+ "providers/tcb/provider.yaml",
137
+ "providers/tcb/deployment.yaml",
138
+ "providers/tcb/preview.yaml",
139
+ ),
140
+ label="Cloud Provider",
141
+ description="TCB (Tencent CloudBase) provider config",
142
+ ),
143
+ CrossCutSpec(
144
+ id="tools",
145
+ paths=(
146
+ "tools/deploy_stack.py",
147
+ "tools/contract_diff.py",
148
+ "tools/generate_shared.py",
149
+ ),
150
+ label="Tooling Scripts",
151
+ description="deploy_stack / contract_diff / generate_shared stubs",
152
+ ),
153
+ )
154
+
155
+
156
+ CROSS_CUTTING_BY_ID: dict[str, CrossCutSpec] = {c.id: c for c in CROSS_CUTTING}
157
+
20
158
 
21
159
  TCB_PROVIDER_YAML = """provider:
22
160
  name: tcb
@@ -149,61 +287,91 @@ def _write(path: Path, content: str) -> None:
149
287
  path.write_text(content.lstrip("\n"), encoding="utf-8")
150
288
 
151
289
 
152
- def _build_project_yaml(active: list[dict]) -> dict:
290
+ def _component_to_dict(c: ComponentSpec) -> dict:
291
+ out = {
292
+ "id": c.id,
293
+ "kind": c.kind,
294
+ "tech": c.tech,
295
+ "owns": c.owns,
296
+ "fr_prefix": c.fr_prefix,
297
+ }
298
+ return out
299
+
300
+
301
+ def _build_project_yaml(active: list[ComponentSpec], cross_cutting_ids: list[str]) -> dict:
302
+ cross_cutting: dict = {"fr_prefix": "INT"}
303
+ if "contracts" in cross_cutting_ids:
304
+ cross_cutting["contracts"] = "contracts/"
305
+ if "shared" in cross_cutting_ids:
306
+ cross_cutting["shared_types"] = "packages/shared/"
153
307
  return {
154
308
  "stack": {
155
309
  "topology": "monorepo",
156
- "components": [
157
- {
158
- "id": c["id"],
159
- "kind": c["kind"],
160
- "tech": c["tech"],
161
- "owns": c["owns"],
162
- "fr_prefix": c["fr_prefix"],
163
- }
164
- | ({"default_language": c["default_language"]} if c.get("default_language") else {})
165
- | ({"default_ui_framework": c["default_ui_framework"]} if c.get("default_ui_framework") else {})
166
- for c in active
167
- ],
168
- "cross_cutting": {
169
- "fr_prefix": "INT",
170
- "contracts": "contracts/",
171
- "shared_types": "packages/shared/",
172
- },
310
+ "components": [_component_to_dict(c) for c in active],
311
+ "cross_cutting": cross_cutting,
173
312
  }
174
313
  }
175
314
 
176
315
 
177
- def _build_component_table(active: list[dict]) -> str:
316
+ def _build_component_table(active: list[ComponentSpec]) -> str:
317
+ if not active:
318
+ return "| (none) | - | - | - | - |"
178
319
  return "\n".join(
179
- f"| {c['id']:8s} | {c['fr_prefix'] + '-FR-*':14s} | {c.get('default_language', '-'):17s} | {c.get('default_ui_framework', '-'):21s} | {c['owns']:20s} |"
320
+ f"| {c.id:8s} | {c.fr_prefix + '-FR-*':14s} | {'-':17s} | {'-':21s} | {c.owns:20s} |"
180
321
  for c in active
181
322
  )
182
323
 
183
324
 
184
- def init_dlc_project(
185
- workspace_root: Path,
325
+ def _scaffold_component(c: ComponentSpec, root: Path) -> None:
326
+ comp_dir = root / c.owns
327
+ _mkdir(comp_dir / "src")
328
+ _mkdir(comp_dir / "tests" / "unit")
329
+ _mkdir(comp_dir / "tests" / "e2e")
330
+ _mkdir(comp_dir / "features")
331
+ _gitkeep(comp_dir / "src")
332
+ _gitkeep(comp_dir / "tests" / "unit")
333
+ _gitkeep(comp_dir / "tests" / "e2e")
334
+ _gitkeep(comp_dir / "features")
335
+ _gitkeep(comp_dir)
336
+
337
+
338
+ def _scaffold_cross_cutting(cross_ids: list[str], root: Path) -> None:
339
+ for cid in cross_ids:
340
+ spec = CROSS_CUTTING_BY_ID.get(cid)
341
+ if spec is None:
342
+ continue
343
+ for p in spec.paths:
344
+ target = root / p
345
+ if p.endswith((".yaml", ".py", ".md")):
346
+ if p == "contracts/CHANGELOG.md":
347
+ _write(target, CHANGELOG_MD)
348
+ elif p == "providers/tcb/provider.yaml":
349
+ _write(target, TCB_PROVIDER_YAML)
350
+ elif p == "providers/tcb/deployment.yaml":
351
+ _write(target, TCB_DEPLOYMENT_YAML)
352
+ elif p == "providers/tcb/preview.yaml":
353
+ _write(target, TCB_PREVIEW_YAML)
354
+ elif p.startswith("tools/") and p.endswith(".py"):
355
+ _write(target, TOOL_STUB)
356
+ target.chmod(0o755)
357
+ else:
358
+ _write(target, "")
359
+ else:
360
+ _mkdir(target)
361
+ _gitkeep(target)
362
+
363
+
364
+ def _write_project_yaml(
365
+ root: Path,
186
366
  project_name: str,
187
- description: str = "",
188
- ) -> bool:
189
- """Scaffold only the basic project skeleton (root-level files + .cdh/).
190
-
191
- Component directories under apps/ are NOT created; they are added
192
- dynamically when the user selects which components to include.
193
-
194
- Returns True if scaffolding was performed, False if ai-dlc-skill
195
- is not available and the project was left untouched.
196
- """
197
- if not _detect_dlc_skill(workspace_root):
198
- return False
199
-
200
- root = workspace_root.resolve()
201
- active = list(SKILL_YAML_COMPONENTS)
202
-
203
- # --- project.yaml (definition only — no app dirs yet) ---
204
- _write(root / "project.yaml", yaml.dump(_build_project_yaml(active), default_flow_style=False))
205
-
206
- # --- requirements.md ---
367
+ active: list[ComponentSpec],
368
+ cross_cutting_ids: list[str],
369
+ description: str,
370
+ ) -> None:
371
+ _write(
372
+ root / "project.yaml",
373
+ yaml.dump(_build_project_yaml(active, cross_cutting_ids), default_flow_style=False),
374
+ )
207
375
  _write(
208
376
  root / "requirements.md",
209
377
  REQUIREMENTS_MD.format(
@@ -212,101 +380,198 @@ def init_dlc_project(
212
380
  component_table=_build_component_table(active),
213
381
  ),
214
382
  )
215
-
216
- # --- .gitignore ---
217
383
  _write(root / ".gitignore", GITIGNORE_CONTENT)
218
-
219
- # --- CHANGELOG.md (root-level metadata) ---
220
384
  _write(root / "CHANGELOG.md", CHANGELOG_MD)
221
385
 
386
+
387
+ def init_dlc_project(
388
+ workspace_root: Path,
389
+ project_name: str,
390
+ description: str = "",
391
+ ) -> bool:
392
+ """Scaffold project metadata only (no apps/*, no cross-cutting dirs).
393
+
394
+ Writes project.yaml (with empty components list), requirements.md,
395
+ .gitignore, and CHANGELOG.md. The user is expected to add components
396
+ and cross-cutting items later via add_component / add_cross_cutting.
397
+
398
+ Returns True if scaffolding was performed.
399
+
400
+ Raises RuntimeError if ai-dlc-skill is not available — the directory
401
+ is left untouched in that case so callers don't end up with a
402
+ half-initialized project (.cdh/ written but no project.yaml).
403
+ """
404
+ if not _detect_dlc_skill(workspace_root):
405
+ raise RuntimeError(
406
+ "ai-dlc-skill is not available; install it before "
407
+ "initializing a cdh project."
408
+ )
409
+
410
+ root = workspace_root.resolve()
411
+ _write_project_yaml(
412
+ root=root,
413
+ project_name=project_name,
414
+ active=[],
415
+ cross_cutting_ids=[],
416
+ description=description,
417
+ )
222
418
  return True
223
419
 
224
420
 
225
421
  def scaffold_dlc_project(
226
422
  workspace_root: Path,
227
423
  project_name: str,
228
- components: Optional[list[str]] = None,
424
+ components: list[str],
229
425
  description: str = "",
230
426
  ) -> bool:
231
427
  """Scaffold a full ai-dlc-skill monorepo project structure.
232
428
 
429
+ The user must explicitly select at least one application component.
430
+ All cross-cutting items (contracts, shared types, openspec, etc.)
431
+ are always created.
432
+
433
+ Args:
434
+ components: list of component ids (e.g. ["web", "backend"]).
435
+ Must be non-empty.
436
+
233
437
  Returns True if scaffolding was performed, False if ai-dlc-skill
234
438
  is not available and the project was left untouched.
439
+
440
+ Raises ValueError if components is empty or contains unknown ids.
235
441
  """
442
+ if not components:
443
+ raise ValueError(
444
+ "At least one application component is required "
445
+ "(e.g. 'web', 'backend', 'native')."
446
+ )
447
+
448
+ unknown = [c for c in components if c not in COMPONENT_BY_ID]
449
+ if unknown:
450
+ raise ValueError(
451
+ f"Unknown component id(s): {', '.join(unknown)}. "
452
+ f"Valid ids: {', '.join(COMPONENT_BY_ID)}."
453
+ )
454
+
236
455
  if not _detect_dlc_skill(workspace_root):
237
456
  return False
238
457
 
239
458
  root = workspace_root.resolve()
459
+ active = [COMPONENT_BY_ID[cid] for cid in components]
460
+ all_cross_ids = [c.id for c in CROSS_CUTTING]
461
+
462
+ _write_project_yaml(
463
+ root=root,
464
+ project_name=project_name,
465
+ active=active,
466
+ cross_cutting_ids=all_cross_ids,
467
+ description=description,
468
+ )
240
469
 
241
- if components is not None:
242
- active = [c for c in SKILL_YAML_COMPONENTS if c["id"] in components]
243
- else:
244
- active = list(SKILL_YAML_COMPONENTS)
470
+ for c in active:
471
+ _scaffold_component(c, root)
245
472
 
246
- # --- project.yaml ---
247
- _write(root / "project.yaml", yaml.dump(_build_project_yaml(active), default_flow_style=False))
473
+ _scaffold_cross_cutting(all_cross_ids, root)
248
474
 
249
- # --- requirements.md ---
250
- _write(
251
- root / "requirements.md",
252
- REQUIREMENTS_MD.format(
253
- project_name=project_name,
254
- description=description or f"AI-DLC monorepo project: {project_name}",
255
- component_table=_build_component_table(active),
256
- ),
475
+ return True
476
+
477
+
478
+ def add_component(
479
+ workspace_root: Path,
480
+ component_id: str,
481
+ ) -> bool:
482
+ """Add a single application component to an existing project.
483
+
484
+ Creates apps/<owns>/{src,tests/unit,tests/e2e,features} with
485
+ .gitkeep files and updates project.yaml's stack.components list.
486
+
487
+ Returns True if the component was added, False if it was already
488
+ present. Raises ValueError on unknown component id.
489
+ """
490
+ spec = COMPONENT_BY_ID.get(component_id)
491
+ if spec is None:
492
+ raise ValueError(
493
+ f"Unknown component id: {component_id}. "
494
+ f"Valid ids: {', '.join(COMPONENT_BY_ID)}."
495
+ )
496
+
497
+ root = workspace_root.resolve()
498
+ project_yaml = root / "project.yaml"
499
+ if not project_yaml.exists():
500
+ raise FileNotFoundError(
501
+ f"project.yaml not found at {root}. Run 'cdh project init' first."
502
+ )
503
+
504
+ data = yaml.safe_load(project_yaml.read_text(encoding="utf-8")) or {}
505
+ components = data.get("stack", {}).get("components", []) or []
506
+ if any(c.get("id") == component_id for c in components):
507
+ return False
508
+
509
+ _scaffold_component(spec, root)
510
+
511
+ components.append(_component_to_dict(spec))
512
+ data["stack"]["components"] = components
513
+ project_yaml.write_text(
514
+ yaml.dump(data, default_flow_style=False),
515
+ encoding="utf-8",
257
516
  )
517
+ return True
258
518
 
259
- # --- .gitignore ---
260
- _write(root / ".gitignore", GITIGNORE_CONTENT)
261
519
 
262
- # --- apps/{component}/ src/ tests/ features/ ---
263
- for c in active:
264
- comp_dir = root / c["owns"]
265
- _mkdir(comp_dir / "src")
266
- _mkdir(comp_dir / "tests" / "unit")
267
- _mkdir(comp_dir / "tests" / "e2e")
268
- _mkdir(comp_dir / "features")
269
- _gitkeep(comp_dir / "src")
270
- _gitkeep(comp_dir / "tests" / "unit")
271
- _gitkeep(comp_dir / "tests" / "e2e")
272
- _gitkeep(comp_dir / "features")
273
- _gitkeep(comp_dir)
274
-
275
- # --- contracts/{api,events}/ + CHANGELOG.md ---
276
- _mkdir(root / "contracts" / "api")
277
- _mkdir(root / "contracts" / "events")
278
- _gitkeep(root / "contracts" / "api")
279
- _gitkeep(root / "contracts" / "events")
280
- _write(root / "contracts" / "CHANGELOG.md", CHANGELOG_MD)
281
-
282
- # --- packages/shared/ ---
283
- _mkdir(root / "packages" / "shared")
284
- _gitkeep(root / "packages" / "shared")
285
-
286
- # --- features/cross-stack/ ---
287
- _mkdir(root / "features" / "cross-stack")
288
- _gitkeep(root / "features" / "cross-stack")
289
-
290
- # --- tests/{contract,cross-stack}/ ---
291
- _mkdir(root / "tests" / "contract")
292
- _mkdir(root / "tests" / "cross-stack")
293
- _gitkeep(root / "tests" / "contract")
294
- _gitkeep(root / "tests" / "cross-stack")
295
-
296
- # --- openspec/changes/ ---
297
- _mkdir(root / "openspec" / "changes")
298
- _gitkeep(root / "openspec" / "changes")
299
-
300
- # --- providers/tcb/ ---
301
- _mkdir(root / "providers" / "tcb")
302
- _write(root / "providers" / "tcb" / "provider.yaml", TCB_PROVIDER_YAML)
303
- _write(root / "providers" / "tcb" / "deployment.yaml", TCB_DEPLOYMENT_YAML)
304
- _write(root / "providers" / "tcb" / "preview.yaml", TCB_PREVIEW_YAML)
305
-
306
- # --- tools/ stub scripts ---
307
- _mkdir(root / "tools")
308
- for tool in ("deploy_stack.py", "contract_diff.py", "generate_shared.py"):
309
- _write(root / "tools" / tool, TOOL_STUB)
310
- (root / "tools" / tool).chmod(0o755)
520
+ def add_cross_cutting(
521
+ workspace_root: Path,
522
+ cross_id: str,
523
+ ) -> bool:
524
+ """Add a single cross-cutting item to an existing project.
311
525
 
526
+ Creates the relevant directories/files and updates project.yaml
527
+ so that stack.cross_cutting references the new path.
528
+
529
+ Returns True if the item was added, False if it was already present.
530
+ Raises ValueError on unknown cross-cutting id.
531
+ """
532
+ spec = CROSS_CUTTING_BY_ID.get(cross_id)
533
+ if spec is None:
534
+ raise ValueError(
535
+ f"Unknown cross-cutting id: {cross_id}. "
536
+ f"Valid ids: {', '.join(CROSS_CUTTING_BY_ID)}."
537
+ )
538
+
539
+ root = workspace_root.resolve()
540
+ project_yaml = root / "project.yaml"
541
+ if not project_yaml.exists():
542
+ raise FileNotFoundError(
543
+ f"project.yaml not found at {root}. Run 'cdh project init' first."
544
+ )
545
+
546
+ cross_paths = [root / p for p in spec.paths]
547
+ if all(p.exists() for p in cross_paths):
548
+ return False
549
+
550
+ _scaffold_cross_cutting([cross_id], root)
551
+
552
+ data = yaml.safe_load(project_yaml.read_text(encoding="utf-8")) or {}
553
+ cross_cutting = data.setdefault("stack", {}).setdefault("cross_cutting", {})
554
+ if cross_id == "contracts":
555
+ cross_cutting["contracts"] = "contracts/"
556
+ elif cross_id == "shared":
557
+ cross_cutting["shared_types"] = "packages/shared/"
558
+
559
+ project_yaml.write_text(
560
+ yaml.dump(data, default_flow_style=False),
561
+ encoding="utf-8",
562
+ )
312
563
  return True
564
+
565
+
566
+ __all__ = [
567
+ "COMPONENTS",
568
+ "COMPONENT_BY_ID",
569
+ "CROSS_CUTTING",
570
+ "CROSS_CUTTING_BY_ID",
571
+ "ComponentSpec",
572
+ "CrossCutSpec",
573
+ "init_dlc_project",
574
+ "scaffold_dlc_project",
575
+ "add_component",
576
+ "add_cross_cutting",
577
+ ]