lazyopencode 0.1.0__tar.gz → 0.2.0__tar.gz

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 (154) hide show
  1. {lazyopencode-0.1.0 → lazyopencode-0.2.0}/AGENTS.md +3 -12
  2. {lazyopencode-0.1.0 → lazyopencode-0.2.0}/PKG-INFO +28 -25
  3. lazyopencode-0.2.0/README.md +88 -0
  4. lazyopencode-0.2.0/artifacts/demo.png +0 -0
  5. {lazyopencode-0.1.0 → lazyopencode-0.2.0}/pyproject.toml +2 -0
  6. {lazyopencode-0.1.0 → lazyopencode-0.2.0}/src/lazyopencode/__init__.py +12 -1
  7. {lazyopencode-0.1.0 → lazyopencode-0.2.0}/src/lazyopencode/_version.py +2 -2
  8. {lazyopencode-0.1.0 → lazyopencode-0.2.0}/src/lazyopencode/app.py +152 -23
  9. {lazyopencode-0.1.0 → lazyopencode-0.2.0}/src/lazyopencode/bindings.py +2 -0
  10. {lazyopencode-0.1.0 → lazyopencode-0.2.0}/src/lazyopencode/mixins/help.py +2 -0
  11. {lazyopencode-0.1.0 → lazyopencode-0.2.0}/src/lazyopencode/mixins/navigation.py +21 -26
  12. {lazyopencode-0.1.0 → lazyopencode-0.2.0}/src/lazyopencode/models/customization.py +35 -4
  13. lazyopencode-0.2.0/src/lazyopencode/services/claude_code/__init__.py +9 -0
  14. lazyopencode-0.2.0/src/lazyopencode/services/claude_code/discovery.py +158 -0
  15. lazyopencode-0.2.0/src/lazyopencode/services/claude_code/parsers/__init__.py +7 -0
  16. lazyopencode-0.2.0/src/lazyopencode/services/claude_code/parsers/agent.py +58 -0
  17. lazyopencode-0.2.0/src/lazyopencode/services/claude_code/parsers/command.py +75 -0
  18. lazyopencode-0.2.0/src/lazyopencode/services/claude_code/parsers/skill.py +130 -0
  19. lazyopencode-0.2.0/src/lazyopencode/services/claude_code/plugin_loader.py +164 -0
  20. {lazyopencode-0.1.0 → lazyopencode-0.2.0}/src/lazyopencode/services/discovery.py +25 -4
  21. lazyopencode-0.2.0/src/lazyopencode/services/writer.py +119 -0
  22. {lazyopencode-0.1.0 → lazyopencode-0.2.0}/src/lazyopencode/widgets/app_footer.py +1 -1
  23. lazyopencode-0.2.0/src/lazyopencode/widgets/level_selector.py +130 -0
  24. lazyopencode-0.2.0/tests/spec/SPEC.md +129 -0
  25. lazyopencode-0.2.0/tests/spec/__init__.py +1 -0
  26. lazyopencode-0.2.0/tests/spec/conftest.py +208 -0
  27. lazyopencode-0.2.0/tests/spec/scenarios/complex/.opencode/agent/reviewer.md +14 -0
  28. lazyopencode-0.2.0/tests/spec/scenarios/complex/.opencode/command/verify.md +16 -0
  29. lazyopencode-0.2.0/tests/spec/scenarios/complex/.opencode/plugin/metrics.ts +18 -0
  30. lazyopencode-0.2.0/tests/spec/scenarios/complex/.opencode/skill/deploy-helper/SKILL.md +19 -0
  31. lazyopencode-0.2.0/tests/spec/scenarios/complex/.opencode/skill/deploy-helper/scripts/deploy.sh +6 -0
  32. lazyopencode-0.2.0/tests/spec/scenarios/complex/.opencode/tool/search.ts +16 -0
  33. lazyopencode-0.2.0/tests/spec/scenarios/complex/AGENTS.md +14 -0
  34. lazyopencode-0.2.0/tests/spec/scenarios/complex/README.md +51 -0
  35. lazyopencode-0.2.0/tests/spec/scenarios/complex/docs/guidelines.md +12 -0
  36. lazyopencode-0.2.0/tests/spec/scenarios/complex/opencode.json +39 -0
  37. lazyopencode-0.2.0/tests/spec/scenarios/complex/prompts/auditor.txt +7 -0
  38. lazyopencode-0.2.0/tests/spec/scenarios/file_commands/.opencode/command/deploy.md +11 -0
  39. lazyopencode-0.2.0/tests/spec/scenarios/file_commands/.opencode/command/greet.md +10 -0
  40. lazyopencode-0.2.0/tests/spec/scenarios/file_commands/README.md +13 -0
  41. lazyopencode-0.2.0/tests/spec/scenarios/file_references/README.md +14 -0
  42. lazyopencode-0.2.0/tests/spec/scenarios/file_references/opencode.json +16 -0
  43. lazyopencode-0.2.0/tests/spec/scenarios/file_references/prompts/agent.txt +8 -0
  44. lazyopencode-0.2.0/tests/spec/scenarios/file_references/templates/cmd.txt +7 -0
  45. lazyopencode-0.2.0/tests/spec/scenarios/inline_commands/README.md +14 -0
  46. lazyopencode-0.2.0/tests/spec/scenarios/inline_commands/opencode.json +14 -0
  47. lazyopencode-0.2.0/tests/spec/scenarios/minimal/AGENTS.md +7 -0
  48. lazyopencode-0.2.0/tests/spec/scenarios/minimal/README.md +11 -0
  49. lazyopencode-0.2.0/tests/spec/scenarios/mixed_config/.opencode/agent/file-agent.md +7 -0
  50. lazyopencode-0.2.0/tests/spec/scenarios/mixed_config/.opencode/command/file-cmd.md +6 -0
  51. lazyopencode-0.2.0/tests/spec/scenarios/mixed_config/AGENTS.md +7 -0
  52. lazyopencode-0.2.0/tests/spec/scenarios/mixed_config/README.md +18 -0
  53. lazyopencode-0.2.0/tests/spec/scenarios/mixed_config/opencode.json +22 -0
  54. lazyopencode-0.2.0/tests/spec/scenarios/rich_opencode_json/README.md +46 -0
  55. lazyopencode-0.2.0/tests/spec/scenarios/rich_opencode_json/docs/api-standards.md +17 -0
  56. {lazyopencode-0.1.0/reference-customizations → lazyopencode-0.2.0/tests/spec/scenarios/rich_opencode_json}/opencode.json +2 -4
  57. {lazyopencode-0.1.0/reference-customizations → lazyopencode-0.2.0/tests/spec/scenarios/rich_opencode_json}/prompts/security-audit.txt +1 -1
  58. {lazyopencode-0.1.0/reference-customizations → lazyopencode-0.2.0/tests/spec/scenarios/rich_opencode_json}/scripts/fake-lint.sh +0 -1
  59. lazyopencode-0.2.0/tests/spec/scenarios/skills_with_tree/.opencode/skill/my-skill/SKILL.md +15 -0
  60. lazyopencode-0.2.0/tests/spec/scenarios/skills_with_tree/.opencode/skill/my-skill/docs/guide.md +11 -0
  61. lazyopencode-0.2.0/tests/spec/scenarios/skills_with_tree/.opencode/skill/my-skill/scripts/run.sh +5 -0
  62. lazyopencode-0.2.0/tests/spec/scenarios/skills_with_tree/README.md +14 -0
  63. lazyopencode-0.2.0/tests/spec/test_scenarios.py +709 -0
  64. {lazyopencode-0.1.0 → lazyopencode-0.2.0}/uv.lock +22 -0
  65. lazyopencode-0.1.0/.opencode/command/commit.md +0 -36
  66. lazyopencode-0.1.0/.opencode/plugin/compaction.ts +0 -21
  67. lazyopencode-0.1.0/.opencode/plugin/custom-tools.ts +0 -31
  68. lazyopencode-0.1.0/.opencode/plugin/env-protection.ts +0 -21
  69. lazyopencode-0.1.0/.opencode/plugin/notification.ts +0 -24
  70. lazyopencode-0.1.0/.opencode/tool/database.ts +0 -12
  71. lazyopencode-0.1.0/.opencode/tool/math.ts +0 -39
  72. lazyopencode-0.1.0/.opencode/tool/project-info.ts +0 -11
  73. lazyopencode-0.1.0/.opencode/tool/python-runner.ts +0 -17
  74. lazyopencode-0.1.0/README.md +0 -86
  75. lazyopencode-0.1.0/reference-customizations/.opencode/agent/researcher.md +0 -12
  76. lazyopencode-0.1.0/reference-customizations/.opencode/command/verify.md +0 -11
  77. lazyopencode-0.1.0/reference-customizations/.opencode/plugin/compaction.ts +0 -21
  78. lazyopencode-0.1.0/reference-customizations/.opencode/plugin/custom-tools.ts +0 -31
  79. lazyopencode-0.1.0/reference-customizations/.opencode/plugin/env-protection.ts +0 -21
  80. lazyopencode-0.1.0/reference-customizations/.opencode/plugin/notification.ts +0 -24
  81. lazyopencode-0.1.0/reference-customizations/.opencode/skill/architecture-audit/SKILL.md +0 -15
  82. lazyopencode-0.1.0/reference-customizations/.opencode/skill/architecture-audit/references/patterns.md +0 -10
  83. lazyopencode-0.1.0/reference-customizations/.opencode/skill/simple-formatter/SKILL.md +0 -10
  84. lazyopencode-0.1.0/reference-customizations/.opencode/tool/database.ts +0 -12
  85. lazyopencode-0.1.0/reference-customizations/.opencode/tool/math.ts +0 -39
  86. lazyopencode-0.1.0/reference-customizations/.opencode/tool/project-info.ts +0 -11
  87. lazyopencode-0.1.0/reference-customizations/.opencode/tool/python-runner.ts +0 -17
  88. lazyopencode-0.1.0/reference-customizations/AGENTS.md +0 -12
  89. lazyopencode-0.1.0/reference-customizations/TODO.md +0 -10
  90. lazyopencode-0.1.0/reference-customizations/docs/api-standards.md +0 -82
  91. lazyopencode-0.1.0/reference-customizations/docs/testing-guidelines.md +0 -70
  92. lazyopencode-0.1.0/reference-customizations/docs/typescript-guidelines.md +0 -56
  93. {lazyopencode-0.1.0 → lazyopencode-0.2.0}/.github/release-drafter.yml +0 -0
  94. {lazyopencode-0.1.0 → lazyopencode-0.2.0}/.github/workflows/ci.yml +0 -0
  95. {lazyopencode-0.1.0 → lazyopencode-0.2.0}/.github/workflows/publish.yml +0 -0
  96. {lazyopencode-0.1.0 → lazyopencode-0.2.0}/.github/workflows/release-drafter.yml +0 -0
  97. {lazyopencode-0.1.0 → lazyopencode-0.2.0}/.gitignore +0 -0
  98. {lazyopencode-0.1.0 → lazyopencode-0.2.0}/.opencode/command/run-quality-gates.md +0 -0
  99. {lazyopencode-0.1.0 → lazyopencode-0.2.0}/.opencode/skill/quality-gates/SKILL.md +0 -0
  100. {lazyopencode-0.1.0 → lazyopencode-0.2.0}/.pre-commit-config.yaml +0 -0
  101. {lazyopencode-0.1.0 → lazyopencode-0.2.0}/LICENSE +0 -0
  102. {lazyopencode-0.1.0 → lazyopencode-0.2.0}/artifacts/lazyclaude-reference.png +0 -0
  103. {lazyopencode-0.1.0 → lazyopencode-0.2.0}/opencode.json +0 -0
  104. {lazyopencode-0.1.0 → lazyopencode-0.2.0}/scripts/check_quality.sh +0 -0
  105. {lazyopencode-0.1.0 → lazyopencode-0.2.0}/src/lazyopencode/__main__.py +0 -0
  106. {lazyopencode-0.1.0 → lazyopencode-0.2.0}/src/lazyopencode/mixins/filtering.py +0 -0
  107. {lazyopencode-0.1.0 → lazyopencode-0.2.0}/src/lazyopencode/models/__init__.py +0 -0
  108. {lazyopencode-0.1.0 → lazyopencode-0.2.0}/src/lazyopencode/services/__init__.py +0 -0
  109. {lazyopencode-0.1.0 → lazyopencode-0.2.0}/src/lazyopencode/services/gitignore_filter.py +0 -0
  110. {lazyopencode-0.1.0 → lazyopencode-0.2.0}/src/lazyopencode/services/parsers/__init__.py +0 -0
  111. {lazyopencode-0.1.0 → lazyopencode-0.2.0}/src/lazyopencode/services/parsers/agent.py +0 -0
  112. {lazyopencode-0.1.0 → lazyopencode-0.2.0}/src/lazyopencode/services/parsers/command.py +0 -0
  113. {lazyopencode-0.1.0 → lazyopencode-0.2.0}/src/lazyopencode/services/parsers/mcp.py +0 -0
  114. {lazyopencode-0.1.0 → lazyopencode-0.2.0}/src/lazyopencode/services/parsers/plugin.py +0 -0
  115. {lazyopencode-0.1.0 → lazyopencode-0.2.0}/src/lazyopencode/services/parsers/rules.py +0 -0
  116. {lazyopencode-0.1.0 → lazyopencode-0.2.0}/src/lazyopencode/services/parsers/skill.py +0 -0
  117. {lazyopencode-0.1.0 → lazyopencode-0.2.0}/src/lazyopencode/services/parsers/tool.py +0 -0
  118. {lazyopencode-0.1.0 → lazyopencode-0.2.0}/src/lazyopencode/styles/app.tcss +0 -0
  119. {lazyopencode-0.1.0 → lazyopencode-0.2.0}/src/lazyopencode/themes.py +0 -0
  120. {lazyopencode-0.1.0 → lazyopencode-0.2.0}/src/lazyopencode/widgets/__init__.py +0 -0
  121. {lazyopencode-0.1.0 → lazyopencode-0.2.0}/src/lazyopencode/widgets/combined_panel.py +0 -0
  122. {lazyopencode-0.1.0 → lazyopencode-0.2.0}/src/lazyopencode/widgets/detail_pane.py +0 -0
  123. {lazyopencode-0.1.0 → lazyopencode-0.2.0}/src/lazyopencode/widgets/filter_input.py +0 -0
  124. {lazyopencode-0.1.0 → lazyopencode-0.2.0}/src/lazyopencode/widgets/helpers/__init__.py +0 -0
  125. {lazyopencode-0.1.0 → lazyopencode-0.2.0}/src/lazyopencode/widgets/helpers/rendering.py +0 -0
  126. {lazyopencode-0.1.0 → lazyopencode-0.2.0}/src/lazyopencode/widgets/status_panel.py +0 -0
  127. {lazyopencode-0.1.0 → lazyopencode-0.2.0}/src/lazyopencode/widgets/type_panel.py +0 -0
  128. {lazyopencode-0.1.0 → lazyopencode-0.2.0}/tests/AGENTS.md +0 -0
  129. {lazyopencode-0.1.0 → lazyopencode-0.2.0}/tests/conftest.py +0 -0
  130. {lazyopencode-0.1.0 → lazyopencode-0.2.0}/tests/integration/__init__.py +0 -0
  131. {lazyopencode-0.1.0 → lazyopencode-0.2.0}/tests/integration/discovery/__init__.py +0 -0
  132. {lazyopencode-0.1.0 → lazyopencode-0.2.0}/tests/integration/discovery/test_agents.py +0 -0
  133. {lazyopencode-0.1.0 → lazyopencode-0.2.0}/tests/integration/discovery/test_commands.py +0 -0
  134. {lazyopencode-0.1.0 → lazyopencode-0.2.0}/tests/integration/discovery/test_full_discovery.py +0 -0
  135. {lazyopencode-0.1.0 → lazyopencode-0.2.0}/tests/integration/discovery/test_mcps.py +0 -0
  136. {lazyopencode-0.1.0 → lazyopencode-0.2.0}/tests/integration/discovery/test_rules.py +0 -0
  137. {lazyopencode-0.1.0 → lazyopencode-0.2.0}/tests/integration/discovery/test_skills.py +0 -0
  138. {lazyopencode-0.1.0 → lazyopencode-0.2.0}/tests/integration/fixtures/agent/explorer.md +0 -0
  139. {lazyopencode-0.1.0 → lazyopencode-0.2.0}/tests/integration/fixtures/command/greet.md +0 -0
  140. {lazyopencode-0.1.0 → lazyopencode-0.2.0}/tests/integration/fixtures/mcp/project-opencode.json +0 -0
  141. {lazyopencode-0.1.0 → lazyopencode-0.2.0}/tests/integration/fixtures/mcp/user-opencode.json +0 -0
  142. {lazyopencode-0.1.0 → lazyopencode-0.2.0}/tests/integration/fixtures/memory/AGENTS.md +0 -0
  143. {lazyopencode-0.1.0 → lazyopencode-0.2.0}/tests/integration/fixtures/project/AGENTS.md +0 -0
  144. {lazyopencode-0.1.0 → lazyopencode-0.2.0}/tests/integration/fixtures/project/agent/reviewer.md +0 -0
  145. {lazyopencode-0.1.0 → lazyopencode-0.2.0}/tests/integration/fixtures/project/command/project-cmd.md +0 -0
  146. {lazyopencode-0.1.0 → lazyopencode-0.2.0}/tests/integration/fixtures/project/docs/guidelines.md +0 -0
  147. {lazyopencode-0.1.0 → lazyopencode-0.2.0}/tests/integration/fixtures/project/skill/project-skill/SKILL.md +0 -0
  148. {lazyopencode-0.1.0 → lazyopencode-0.2.0}/tests/integration/fixtures/project/skill/project-skill/src/helper.py +0 -0
  149. {lazyopencode-0.1.0 → lazyopencode-0.2.0}/tests/integration/fixtures/skill/task-tracker/SKILL.md +0 -0
  150. {lazyopencode-0.1.0 → lazyopencode-0.2.0}/tests/integration/fixtures/skill/task-tracker/reference.md +0 -0
  151. {lazyopencode-0.1.0 → lazyopencode-0.2.0}/tests/integration/fixtures/skill/task-tracker/scripts/run.sh +0 -0
  152. {lazyopencode-0.1.0 → lazyopencode-0.2.0}/tests/test_inline.py +0 -0
  153. {lazyopencode-0.1.0 → lazyopencode-0.2.0}/tests/test_version.py +0 -0
  154. {lazyopencode-0.1.0 → lazyopencode-0.2.0}/tests/unit/__init__.py +0 -0
@@ -50,7 +50,7 @@ src/lazyopencode/
50
50
  ├── models/ # Data models (Customization, ConfigLevel, etc.)
51
51
  ├── services/ # Business logic
52
52
  │ ├── discovery.py # Finds customizations on disk
53
- │ ├── filter.py # Filters by level/query
53
+ │ ├── gitignore_filter.py # Gitignore-aware filtering
54
54
  │ └── parsers/ # Type-specific parsers
55
55
  ├── widgets/ # Textual UI components
56
56
  ├── mixins/ # App functionality mixins
@@ -99,7 +99,7 @@ The application discovers customizations from these locations:
99
99
 
100
100
  ### Services
101
101
  - `ConfigDiscoveryService` - Scans filesystem, uses parsers
102
- - `FilterService` - Filters by level and search query
102
+ - `GitignoreFilter` - Filters paths using gitignore rules
103
103
  - `ICustomizationParser` - Protocol for type-specific parsers
104
104
 
105
105
  ### Widgets
@@ -111,7 +111,7 @@ The application discovers customizations from these locations:
111
111
 
112
112
  ### Mixins
113
113
  - `NavigationMixin` - Panel focus, cursor movement
114
- - `FilterMixin` - Level filters, search
114
+ - `FilteringMixin` - Level filters, search
115
115
  - `HelpMixin` - Help overlay
116
116
 
117
117
  ## Testing
@@ -162,12 +162,3 @@ uv run pytest tests/unit/test_parsers.py
162
162
  2. Add styles to `styles/app.tcss`
163
163
  3. Compose in `app.py`
164
164
 
165
- ## Planning Documents
166
-
167
- See `_plans/` directory for detailed specifications:
168
- - `00-overview.md` - Project overview
169
- - `01-architecture.md` - Architecture decisions
170
- - `02-customization-types.md` - OpenCode customization mapping
171
- - `03-implementation-phases.md` - Implementation plan
172
- - `04-file-structure.md` - File structure
173
- - `05-agents-md-template.md` - This template
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lazyopencode
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: A lazygit-style TUI for visualizing OpenCode customizations
5
5
  Project-URL: Homepage, https://github.com/nikiforovall/lazyopencode
6
6
  Project-URL: Repository, https://github.com/nikiforovall/lazyopencode
@@ -19,6 +19,7 @@ Classifier: Programming Language :: Python :: 3.12
19
19
  Classifier: Topic :: Software Development :: User Interfaces
20
20
  Requires-Python: >=3.11
21
21
  Requires-Dist: pathspec>=0.12.0
22
+ Requires-Dist: pyperclip>=1.9.0
22
23
  Requires-Dist: pyyaml>=6.0
23
24
  Requires-Dist: rich>=13.0.0
24
25
  Requires-Dist: textual>=0.89.0
@@ -34,7 +35,7 @@ Description-Content-Type: text/markdown
34
35
 
35
36
  A keyboard-driven TUI for managing OpenCode customizations.
36
37
 
37
- <!-- ![LazyOpenCode Screenshot](docs/screenshot.png) -->
38
+ ![LazyOpenCode Screenshot](artifacts/demo.png)
38
39
 
39
40
  ## Features
40
41
 
@@ -58,34 +59,36 @@ pip install lazyopencode
58
59
 
59
60
  ## Keyboard Shortcuts
60
61
 
61
- | Key | Action |
62
- | --------- | -------------- |
63
- | `q` | Quit |
64
- | `1` | Commands panel |
65
- | `2` | Agents panel |
66
- | `3` | Skills panel |
67
- | `4` | Rules panel |
68
- | `5` | MCPs panel |
69
- | `6` | Plugins panel |
70
- | `j` / `↓` | Move down |
71
- | `k` / `↑` | Move up |
72
- | `Tab` | Next panel |
73
- | `e` | Edit selected |
74
- | `ctrl`+`u` | User Config |
75
- | `?` | Help |
62
+ | Key | Action |
63
+ | ---------- | ---------------- |
64
+ | `j` / `↓` | Move down |
65
+ | `k` / `↑` | Move up |
66
+ | `Tab` | Next panel |
67
+ | `[` / `]` | Prev/Next view |
68
+ | `1`-`7` | Jump to panel |
69
+ | `a` | All filter |
70
+ | `g` | Global filter |
71
+ | `p` | Project filter |
72
+ | `/` | Search |
73
+ | `e` | Edit selected |
74
+ | `r` | Refresh |
75
+ | `ctrl+u` | User Config |
76
+ | `?` | Help |
77
+ | `q` | Quit |
76
78
 
77
79
  ## Configuration Paths
78
80
 
79
81
  LazyOpenCode discovers customizations from:
80
82
 
81
- | Type | Global | Project |
82
- | -------- | ------------------------------ | -------------------- |
83
- | Commands | `~/.config/opencode/command/` | `.opencode/command/` |
84
- | Agents | `~/.config/opencode/agent/` | `.opencode/agent/` |
85
- | Skills | `~/.config/opencode/skill/` | `.opencode/skill/` |
86
- | Rules | `~/.config/opencode/AGENTS.md` | `AGENTS.md` |
87
- | MCPs | `opencode.json` | `opencode.json` |
88
- | Plugins | `~/.config/opencode/plugin/` | `.opencode/plugin/` |
83
+ | Type | Global | Project |
84
+ | -------- | ---------------------------------- | -------------------- |
85
+ | Commands | `~/.config/opencode/command/` | `.opencode/command/` |
86
+ | Agents | `~/.config/opencode/agent/` | `.opencode/agent/` |
87
+ | Skills | `~/.config/opencode/skill/` | `.opencode/skill/` |
88
+ | Rules | `~/.config/opencode/AGENTS.md` | `AGENTS.md` |
89
+ | MCPs | `~/.config/opencode/opencode.json` | `opencode.json` |
90
+ | Tools | `~/.config/opencode/tool/` | `.opencode/tool/` |
91
+ | Plugins | `~/.config/opencode/plugin/` | `.opencode/plugin/` |
89
92
 
90
93
  ## Inspired By
91
94
 
@@ -0,0 +1,88 @@
1
+ # LazyOpenCode
2
+
3
+ A keyboard-driven TUI for managing OpenCode customizations.
4
+
5
+ ![LazyOpenCode Screenshot](artifacts/demo.png)
6
+
7
+ ## Features
8
+
9
+ - Visual discovery of all OpenCode customizations
10
+ - Keyboard-driven navigation (lazygit-inspired)
11
+ - View commands, agents, skills, rules, MCPs, and plugins
12
+ - Filter by configuration level (global/project)
13
+ - Search within customizations
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ uvx lazyopencode
19
+ ```
20
+
21
+ Or install with pip:
22
+
23
+ ```bash
24
+ pip install lazyopencode
25
+ ```
26
+
27
+ ## Keyboard Shortcuts
28
+
29
+ | Key | Action |
30
+ | ---------- | ---------------- |
31
+ | `j` / `↓` | Move down |
32
+ | `k` / `↑` | Move up |
33
+ | `Tab` | Next panel |
34
+ | `[` / `]` | Prev/Next view |
35
+ | `1`-`7` | Jump to panel |
36
+ | `a` | All filter |
37
+ | `g` | Global filter |
38
+ | `p` | Project filter |
39
+ | `/` | Search |
40
+ | `e` | Edit selected |
41
+ | `r` | Refresh |
42
+ | `ctrl+u` | User Config |
43
+ | `?` | Help |
44
+ | `q` | Quit |
45
+
46
+ ## Configuration Paths
47
+
48
+ LazyOpenCode discovers customizations from:
49
+
50
+ | Type | Global | Project |
51
+ | -------- | ---------------------------------- | -------------------- |
52
+ | Commands | `~/.config/opencode/command/` | `.opencode/command/` |
53
+ | Agents | `~/.config/opencode/agent/` | `.opencode/agent/` |
54
+ | Skills | `~/.config/opencode/skill/` | `.opencode/skill/` |
55
+ | Rules | `~/.config/opencode/AGENTS.md` | `AGENTS.md` |
56
+ | MCPs | `~/.config/opencode/opencode.json` | `opencode.json` |
57
+ | Tools | `~/.config/opencode/tool/` | `.opencode/tool/` |
58
+ | Plugins | `~/.config/opencode/plugin/` | `.opencode/plugin/` |
59
+
60
+ ## Inspired By
61
+
62
+ - [LazyClaude](https://github.com/NikiforovAll/lazyclaude) - Similar TUI for Claude Code
63
+ - [Lazygit](https://github.com/jesseduffield/lazygit) - Keyboard-driven Git TUI
64
+ - [OpenCode](https://opencode.ai) - AI coding agent
65
+
66
+
67
+ ## Development
68
+
69
+ ```bash
70
+ # Clone and install
71
+ git clone https://github.com/yourusername/lazyopencode
72
+ cd lazyopencode
73
+ uv sync
74
+
75
+ # Run
76
+ uv run lazyopencode
77
+
78
+ # Run tests
79
+ uv run pytest
80
+
81
+ # Lint and format
82
+ uv run ruff check .
83
+ uv run ruff format .
84
+ ```
85
+
86
+ ## License
87
+
88
+ MIT
Binary file
@@ -26,6 +26,7 @@ dependencies = [
26
26
  "rich>=13.0.0",
27
27
  "pyyaml>=6.0",
28
28
  "pathspec>=0.12.0",
29
+ "pyperclip>=1.9.0",
29
30
  ]
30
31
 
31
32
  [project.urls]
@@ -108,5 +109,6 @@ dev = [
108
109
  "pytest>=9.0.1",
109
110
  "pytest-asyncio>=1.3.0",
110
111
  "ruff>=0.14.8",
112
+ "types-pyperclip>=1.9.0",
111
113
  "types-pyyaml>=6.0.12.20250915",
112
114
  ]
@@ -38,11 +38,22 @@ def main() -> None:
38
38
  help="Override user config path (default: ~/.config/opencode)",
39
39
  )
40
40
 
41
+ parser.add_argument(
42
+ "--claude-code",
43
+ action="store_true",
44
+ default=False,
45
+ help="Enable Claude Code customizations discovery (from ~/.claude/)",
46
+ )
47
+
41
48
  args = parser.parse_args()
42
49
 
43
50
  # Handle directory argument - resolve to absolute path
44
51
  project_root = args.directory.resolve() if args.directory else None
45
52
  user_config = args.user_config.resolve() if args.user_config else None
46
53
 
47
- app = create_app(project_root=project_root, global_config_path=user_config)
54
+ app = create_app(
55
+ project_root=project_root,
56
+ global_config_path=user_config,
57
+ enable_claude_code=args.claude_code,
58
+ )
48
59
  app.run()
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '0.1.0'
32
- __version_tuple__ = version_tuple = (0, 1, 0)
31
+ __version__ = version = '0.2.0'
32
+ __version_tuple__ = version_tuple = (0, 2, 0)
33
33
 
34
34
  __commit_id__ = commit_id = None
@@ -16,6 +16,7 @@ from lazyopencode.mixins.help import HelpMixin
16
16
  from lazyopencode.mixins.navigation import NavigationMixin
17
17
  from lazyopencode.models.customization import (
18
18
  ConfigLevel,
19
+ ConfigSource,
19
20
  Customization,
20
21
  CustomizationType,
21
22
  )
@@ -25,6 +26,7 @@ from lazyopencode.widgets.app_footer import AppFooter
25
26
  from lazyopencode.widgets.combined_panel import CombinedPanel
26
27
  from lazyopencode.widgets.detail_pane import MainPane
27
28
  from lazyopencode.widgets.filter_input import FilterInput
29
+ from lazyopencode.widgets.level_selector import LevelSelector
28
30
  from lazyopencode.widgets.status_panel import StatusPanel
29
31
  from lazyopencode.widgets.type_panel import TypePanel
30
32
 
@@ -44,6 +46,7 @@ class LazyOpenCode(App, NavigationMixin, FilteringMixin, HelpMixin):
44
46
  discovery_service: ConfigDiscoveryService | None = None,
45
47
  project_root: Path | None = None,
46
48
  global_config_path: Path | None = None,
49
+ enable_claude_code: bool = False,
47
50
  ) -> None:
48
51
  """Initialize LazyOpenCode application."""
49
52
  super().__init__()
@@ -51,6 +54,7 @@ class LazyOpenCode(App, NavigationMixin, FilteringMixin, HelpMixin):
51
54
  self._discovery_service = discovery_service or ConfigDiscoveryService(
52
55
  project_root=project_root,
53
56
  global_config_path=global_config_path,
57
+ enable_claude_code=enable_claude_code,
54
58
  )
55
59
  self._customizations: list[Customization] = []
56
60
  self._level_filter: ConfigLevel | None = None
@@ -60,7 +64,9 @@ class LazyOpenCode(App, NavigationMixin, FilteringMixin, HelpMixin):
60
64
  self._main_pane: MainPane | None = None
61
65
  self._filter_input: FilterInput | None = None
62
66
  self._app_footer: AppFooter | None = None
67
+ self._level_selector: LevelSelector | None = None
63
68
  self._last_focused_panel: TypePanel | CombinedPanel | None = None
69
+ self._pending_customization: Customization | None = None
64
70
 
65
71
  def compose(self) -> ComposeResult:
66
72
  """Compose the application layout."""
@@ -68,16 +74,17 @@ class LazyOpenCode(App, NavigationMixin, FilteringMixin, HelpMixin):
68
74
  self._status_panel = StatusPanel(id="status-panel")
69
75
  yield self._status_panel
70
76
 
71
- # [1]+[2] Combined Panel: Commands, Agents
72
- cp1 = CombinedPanel(
73
- tabs=[
74
- (CustomizationType.COMMAND, 1, "Commands"),
75
- (CustomizationType.AGENT, 2, "Agents"),
76
- ],
77
- id="panel-combined-1",
78
- )
79
- self._panels.append(cp1)
80
- yield cp1
77
+ # [1] Type Panel: Commands
78
+ tp_cmd = TypePanel(CustomizationType.COMMAND, id="panel-command")
79
+ tp_cmd.panel_number = 1
80
+ self._panels.append(tp_cmd)
81
+ yield tp_cmd
82
+
83
+ # [2] Type Panel: Agents
84
+ tp_agent = TypePanel(CustomizationType.AGENT, id="panel-agent")
85
+ tp_agent.panel_number = 2
86
+ self._panels.append(tp_agent)
87
+ yield tp_agent
81
88
 
82
89
  # [3] Type Panel: Skills
83
90
  tp_skills = TypePanel(CustomizationType.SKILL, id="panel-skill")
@@ -85,23 +92,18 @@ class LazyOpenCode(App, NavigationMixin, FilteringMixin, HelpMixin):
85
92
  self._panels.append(tp_skills)
86
93
  yield tp_skills
87
94
 
88
- # [4] Type Panel: Agent Memory (Rules)
89
- tp_rules = TypePanel(CustomizationType.RULES, id="panel-rules")
90
- tp_rules.panel_number = 4
91
- self._panels.append(tp_rules)
92
- yield tp_rules
93
-
94
- # [5]+[6]+[7] Combined Panel: MCPs, Tools, Plugins
95
- cp2 = CombinedPanel(
95
+ # [4]+[5]+[6]+[7] Combined Panel: Memory, MCPs, Tools, Plugins
96
+ cp = CombinedPanel(
96
97
  tabs=[
98
+ (CustomizationType.RULES, 4, "Memory"),
97
99
  (CustomizationType.MCP, 5, "MCPs"),
98
100
  (CustomizationType.TOOL, 6, "Tools"),
99
101
  (CustomizationType.PLUGIN, 7, "Plugins"),
100
102
  ],
101
- id="panel-combined-2",
103
+ id="panel-combined",
102
104
  )
103
- self._panels.append(cp2)
104
- yield cp2
105
+ self._panels.append(cp)
106
+ yield cp
105
107
 
106
108
  self._main_pane = MainPane(id="main-pane")
107
109
  yield self._main_pane
@@ -109,6 +111,9 @@ class LazyOpenCode(App, NavigationMixin, FilteringMixin, HelpMixin):
109
111
  self._filter_input = FilterInput(id="filter-input")
110
112
  yield self._filter_input
111
113
 
114
+ self._level_selector = LevelSelector(id="level-selector")
115
+ yield self._level_selector
116
+
112
117
  self._app_footer = AppFooter(id="app-footer")
113
118
  yield self._app_footer
114
119
 
@@ -121,6 +126,9 @@ class LazyOpenCode(App, NavigationMixin, FilteringMixin, HelpMixin):
121
126
  self._update_status_panel()
122
127
  project_name = self._discovery_service.project_root.name
123
128
  self.title = f"{project_name} - LazyOpenCode"
129
+ self.console.set_window_title(self.title)
130
+ if os.name == "nt":
131
+ os.system(f"title {self.title}")
124
132
  # Focus first non-empty panel or first panel
125
133
  if self._panels:
126
134
  self._panels[0].focus()
@@ -154,7 +162,12 @@ class LazyOpenCode(App, NavigationMixin, FilteringMixin, HelpMixin):
154
162
  """Get customizations filtered by current level and search query."""
155
163
  result = self._customizations
156
164
  if self._level_filter:
157
- result = [c for c in result if c.level == self._level_filter]
165
+ # When filtering by level, only show OpenCode items (exclude Claude Code)
166
+ result = [
167
+ c
168
+ for c in result
169
+ if c.level == self._level_filter and c.source == ConfigSource.OPENCODE
170
+ ]
158
171
  if self._search_query:
159
172
  query = self._search_query.lower()
160
173
  result = [c for c in result if query in c.name.lower()]
@@ -297,14 +310,130 @@ class LazyOpenCode(App, NavigationMixin, FilteringMixin, HelpMixin):
297
310
  except Exception as e:
298
311
  self.notify(f"Error opening editor: {e}", severity="error")
299
312
 
313
+ # Copy actions
314
+
315
+ def action_copy_customization(self) -> None:
316
+ """Copy selected customization to another level."""
317
+ panel = self._get_focused_panel()
318
+ customization = None
319
+
320
+ if panel:
321
+ customization = panel.selected_customization
322
+
323
+ if not customization:
324
+ self.notify("No customization selected", severity="warning")
325
+ return
326
+
327
+ # Only allow copying commands, agents, and skills
328
+ copyable_types = (
329
+ CustomizationType.COMMAND,
330
+ CustomizationType.AGENT,
331
+ CustomizationType.SKILL,
332
+ )
333
+ if customization.type not in copyable_types:
334
+ self.notify(
335
+ f"Cannot copy {customization.type_label} customizations",
336
+ severity="warning",
337
+ )
338
+ return
339
+
340
+ available = customization.get_copy_targets()
341
+ if not available:
342
+ self.notify("No available target levels", severity="warning")
343
+ return
344
+
345
+ self._pending_customization = customization
346
+ self._last_focused_panel = panel
347
+ if self._level_selector:
348
+ self._level_selector.show(available, customization.name)
349
+
350
+ def action_copy_path_to_clipboard(self) -> None:
351
+ """Copy path of selected customization to clipboard."""
352
+ panel = self._get_focused_panel()
353
+ customization = None
354
+
355
+ if panel:
356
+ customization = panel.selected_customization
357
+
358
+ if not customization:
359
+ self.notify("No customization selected", severity="warning")
360
+ return
361
+
362
+ file_path = customization.path
363
+ if customization.type == CustomizationType.SKILL:
364
+ file_path = customization.path.parent
365
+
366
+ try:
367
+ import pyperclip
368
+
369
+ pyperclip.copy(str(file_path))
370
+ self.notify(f"Copied: {file_path}", severity="information")
371
+ except ImportError:
372
+ self.notify(
373
+ "pyperclip not installed. Run: pip install pyperclip",
374
+ severity="error",
375
+ )
376
+ except Exception as e:
377
+ self.notify(f"Failed to copy to clipboard: {e}", severity="error")
378
+
379
+ # Level selector message handlers
380
+
381
+ def on_level_selector_level_selected(
382
+ self, message: LevelSelector.LevelSelected
383
+ ) -> None:
384
+ """Handle level selection from the level selector."""
385
+ if self._pending_customization:
386
+ self._handle_copy(self._pending_customization, message.level)
387
+ self._pending_customization = None
388
+ self._restore_focus_after_selector()
389
+
390
+ def on_level_selector_selection_cancelled(
391
+ self,
392
+ message: LevelSelector.SelectionCancelled, # noqa: ARG002
393
+ ) -> None:
394
+ """Handle level selector cancellation."""
395
+ self._pending_customization = None
396
+ self._restore_focus_after_selector()
397
+
398
+ def _handle_copy(
399
+ self, customization: Customization, target_level: ConfigLevel
400
+ ) -> None:
401
+ """Handle copy operation."""
402
+ from lazyopencode.services.writer import CustomizationWriter
403
+
404
+ writer = CustomizationWriter(
405
+ global_config_path=self._discovery_service.global_config_path,
406
+ project_config_path=self._discovery_service.project_config_path,
407
+ )
408
+
409
+ success, msg = writer.copy_customization(customization, target_level)
410
+
411
+ if success:
412
+ self.notify(msg, severity="information")
413
+ self.action_refresh()
414
+ else:
415
+ self.notify(msg, severity="error")
416
+
417
+ def _restore_focus_after_selector(self) -> None:
418
+ """Restore focus to the previously focused panel."""
419
+ if self._last_focused_panel:
420
+ self._last_focused_panel.focus()
421
+ elif self._panels:
422
+ self._panels[0].focus()
423
+
300
424
 
301
425
  def create_app(
302
426
  project_root: Path | None = None,
303
427
  global_config_path: Path | None = None,
428
+ enable_claude_code: bool = False,
304
429
  ) -> LazyOpenCode:
305
430
  """Create application with all dependencies wired."""
306
431
  discovery_service = ConfigDiscoveryService(
307
432
  project_root=project_root,
308
433
  global_config_path=global_config_path,
434
+ enable_claude_code=enable_claude_code,
435
+ )
436
+ return LazyOpenCode(
437
+ discovery_service=discovery_service,
438
+ enable_claude_code=enable_claude_code,
309
439
  )
310
- return LazyOpenCode(discovery_service=discovery_service)
@@ -7,6 +7,8 @@ APP_BINDINGS: list[BindingType] = [
7
7
  Binding("?", "toggle_help", "Help"),
8
8
  Binding("r", "refresh", "Refresh"),
9
9
  Binding("e", "open_in_editor", "Edit"),
10
+ Binding("c", "copy_customization", "Copy"),
11
+ Binding("C", "copy_path_to_clipboard", "Copy Path", key_display="shift+c"),
10
12
  Binding("ctrl+u", "open_user_config", "User Config"),
11
13
  Binding("tab", "focus_next_panel", "Next Panel", show=False),
12
14
  Binding("shift+tab", "focus_previous_panel", "Prev Panel", show=False),
@@ -50,6 +50,8 @@ class HelpMixin:
50
50
  Combined: switch tabs
51
51
 
52
52
  [bold]Actions[/]
53
+ c Copy to level (modal)
54
+ C (shift+c) Copy path to clipboard
53
55
  e Open in $EDITOR
54
56
  ctrl+u Open User Config
55
57
  r Refresh from disk
@@ -80,70 +80,65 @@ class NavigationMixin:
80
80
  prev_panel.focus()
81
81
 
82
82
  def action_focus_panel_1(self) -> None:
83
- """Focus Commands tab in first combined panel."""
83
+ """Focus Commands panel."""
84
84
  app = cast("LazyOpenCode", self)
85
- from lazyopencode.widgets.combined_panel import CombinedPanel
86
-
87
85
  if len(app._panels) > 0:
88
- panel = app._panels[0]
89
- if isinstance(panel, CombinedPanel):
90
- panel.switch_to_tab(0) # Commands tab
91
- panel.focus()
86
+ app._panels[0].focus()
92
87
 
93
88
  def action_focus_panel_2(self) -> None:
94
- """Focus Agents tab in first combined panel."""
89
+ """Focus Agents panel."""
95
90
  app = cast("LazyOpenCode", self)
96
- from lazyopencode.widgets.combined_panel import CombinedPanel
97
-
98
- if len(app._panels) > 0:
99
- panel = app._panels[0]
100
- if isinstance(panel, CombinedPanel):
101
- panel.switch_to_tab(1) # Agents tab
102
- panel.focus()
91
+ if len(app._panels) > 1:
92
+ app._panels[1].focus()
103
93
 
104
94
  def action_focus_panel_3(self) -> None:
105
95
  """Focus Skills panel."""
106
96
  app = cast("LazyOpenCode", self)
107
- if len(app._panels) > 1:
108
- app._panels[1].focus()
97
+ if len(app._panels) > 2:
98
+ app._panels[2].focus()
109
99
 
110
100
  def action_focus_panel_4(self) -> None:
111
- """Focus Agent Memory (Rules) panel."""
101
+ """Focus Agent Memory (Rules) tab in combined panel."""
112
102
  app = cast("LazyOpenCode", self)
113
- if len(app._panels) > 2:
114
- app._panels[2].focus()
103
+ from lazyopencode.widgets.combined_panel import CombinedPanel
104
+
105
+ if len(app._panels) > 3:
106
+ panel = app._panels[3]
107
+ if isinstance(panel, CombinedPanel):
108
+ panel.switch_to_tab(0) # Memory tab
109
+ panel.focus()
115
110
 
116
111
  def action_focus_panel_5(self) -> None:
117
- """Focus MCPs tab in second combined panel."""
112
+ """Focus MCPs tab in combined panel."""
118
113
  app = cast("LazyOpenCode", self)
119
114
  from lazyopencode.widgets.combined_panel import CombinedPanel
120
115
 
121
116
  if len(app._panels) > 3:
122
117
  panel = app._panels[3]
123
118
  if isinstance(panel, CombinedPanel):
124
- panel.switch_to_tab(0) # MCPs tab
119
+ panel.switch_to_tab(1) # MCPs tab
125
120
  panel.focus()
126
121
 
127
122
  def action_focus_panel_6(self) -> None:
128
- """Focus Tools tab in second combined panel."""
123
+ """Focus Tools tab in combined panel."""
129
124
  app = cast("LazyOpenCode", self)
130
125
  from lazyopencode.widgets.combined_panel import CombinedPanel
131
126
 
132
127
  if len(app._panels) > 3:
133
128
  panel = app._panels[3]
134
129
  if isinstance(panel, CombinedPanel):
135
- panel.switch_to_tab(1) # Tools tab
130
+ panel.switch_to_tab(2) # Tools tab
136
131
  panel.focus()
137
132
 
138
133
  def action_focus_panel_7(self) -> None:
139
- """Focus Plugins tab in second combined panel."""
134
+ """Focus Plugins tab in combined panel."""
140
135
  app = cast("LazyOpenCode", self)
141
136
  from lazyopencode.widgets.combined_panel import CombinedPanel
142
137
 
143
138
  if len(app._panels) > 3:
144
139
  panel = app._panels[3]
145
140
  if isinstance(panel, CombinedPanel):
146
- panel.switch_to_tab(2) # Plugins tab
141
+ panel.switch_to_tab(3) # Plugins tab
147
142
  panel.focus()
148
143
 
149
144
  def action_focus_main_pane(self) -> None: