monoco-toolkit 0.2.5__py3-none-any.whl → 0.2.8__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- monoco/core/agent/adapters.py +24 -1
- monoco/core/config.py +77 -17
- monoco/core/integrations.py +8 -0
- monoco/core/lsp.py +7 -0
- monoco/core/output.py +8 -1
- monoco/core/resources/zh/SKILL.md +6 -7
- monoco/core/setup.py +8 -0
- monoco/features/i18n/resources/zh/SKILL.md +5 -5
- monoco/features/issue/commands.py +179 -55
- monoco/features/issue/core.py +263 -124
- monoco/features/issue/domain/__init__.py +0 -0
- monoco/features/issue/domain/lifecycle.py +126 -0
- monoco/features/issue/domain/models.py +170 -0
- monoco/features/issue/domain/parser.py +223 -0
- monoco/features/issue/domain/workspace.py +104 -0
- monoco/features/issue/engine/__init__.py +22 -0
- monoco/features/issue/engine/config.py +172 -0
- monoco/features/issue/engine/machine.py +185 -0
- monoco/features/issue/engine/models.py +18 -0
- monoco/features/issue/linter.py +118 -12
- monoco/features/issue/lsp/__init__.py +3 -0
- monoco/features/issue/lsp/definition.py +72 -0
- monoco/features/issue/models.py +27 -9
- monoco/features/issue/resources/en/AGENTS.md +5 -0
- monoco/features/issue/resources/en/SKILL.md +26 -2
- monoco/features/issue/resources/zh/AGENTS.md +5 -0
- monoco/features/issue/resources/zh/SKILL.md +34 -10
- monoco/features/issue/validator.py +252 -66
- monoco/features/spike/core.py +5 -22
- monoco/features/spike/resources/zh/SKILL.md +2 -2
- monoco/main.py +2 -26
- monoco_toolkit-0.2.8.dist-info/METADATA +136 -0
- {monoco_toolkit-0.2.5.dist-info → monoco_toolkit-0.2.8.dist-info}/RECORD +36 -30
- monoco/features/agent/commands.py +0 -166
- monoco/features/agent/doctor.py +0 -30
- monoco/features/pty/core.py +0 -185
- monoco/features/pty/router.py +0 -138
- monoco/features/pty/server.py +0 -56
- monoco_toolkit-0.2.5.dist-info/METADATA +0 -93
- {monoco_toolkit-0.2.5.dist-info → monoco_toolkit-0.2.8.dist-info}/WHEEL +0 -0
- {monoco_toolkit-0.2.5.dist-info → monoco_toolkit-0.2.8.dist-info}/entry_points.txt +0 -0
- {monoco_toolkit-0.2.5.dist-info → monoco_toolkit-0.2.8.dist-info}/licenses/LICENSE +0 -0
|
@@ -14,16 +14,18 @@ from . import core
|
|
|
14
14
|
|
|
15
15
|
app = typer.Typer(help="Agent-Native Issue Management.")
|
|
16
16
|
backlog_app = typer.Typer(help="Manage backlog operations.")
|
|
17
|
+
lsp_app = typer.Typer(help="LSP Server commands.")
|
|
17
18
|
app.add_typer(backlog_app, name="backlog")
|
|
19
|
+
app.add_typer(lsp_app, name="lsp")
|
|
18
20
|
console = Console()
|
|
19
21
|
|
|
20
22
|
@app.command("create")
|
|
21
23
|
def create(
|
|
22
|
-
type:
|
|
24
|
+
type: str = typer.Argument(..., help="Issue type (epic, feature, chore, fix, etc.)"),
|
|
23
25
|
title: str = typer.Option(..., "--title", "-t", help="Issue title"),
|
|
24
26
|
parent: Optional[str] = typer.Option(None, "--parent", "-p", help="Parent Issue ID"),
|
|
25
27
|
is_backlog: bool = typer.Option(False, "--backlog", help="Create as backlog item"),
|
|
26
|
-
stage: Optional[
|
|
28
|
+
stage: Optional[str] = typer.Option(None, "--stage", help="Issue stage"),
|
|
27
29
|
dependencies: List[str] = typer.Option([], "--dependency", "-d", help="Issue dependency ID(s)"),
|
|
28
30
|
related: List[str] = typer.Option([], "--related", "-r", help="Related Issue ID(s)"),
|
|
29
31
|
subdir: Optional[str] = typer.Option(None, "--subdir", "-s", help="Subdirectory for organization (e.g. 'Backend/Auth')"),
|
|
@@ -32,11 +34,19 @@ def create(
|
|
|
32
34
|
root: Optional[str] = typer.Option(None, "--root", help="Override issues root directory"),
|
|
33
35
|
json: AgentOutput = False,
|
|
34
36
|
):
|
|
37
|
+
"""Create a new issue."""
|
|
35
38
|
"""Create a new issue."""
|
|
36
39
|
config = get_config()
|
|
37
40
|
issues_root = _resolve_issues_root(config, root)
|
|
38
|
-
status =
|
|
41
|
+
status = "backlog" if is_backlog else "open"
|
|
42
|
+
|
|
43
|
+
# Sanitize inputs (strip #)
|
|
44
|
+
if parent and parent.startswith("#"):
|
|
45
|
+
parent = parent[1:]
|
|
39
46
|
|
|
47
|
+
dependencies = [d[1:] if d.startswith("#") else d for d in dependencies]
|
|
48
|
+
related = [r[1:] if r.startswith("#") else r for r in related]
|
|
49
|
+
|
|
40
50
|
if parent:
|
|
41
51
|
parent_path = core.find_issue_path(issues_root, parent)
|
|
42
52
|
if not parent_path:
|
|
@@ -63,11 +73,15 @@ def create(
|
|
|
63
73
|
except ValueError:
|
|
64
74
|
rel_path = path
|
|
65
75
|
|
|
66
|
-
OutputManager.
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
76
|
+
if OutputManager.is_agent_mode():
|
|
77
|
+
OutputManager.print({
|
|
78
|
+
"issue": issue,
|
|
79
|
+
"path": str(rel_path),
|
|
80
|
+
"status": "created"
|
|
81
|
+
})
|
|
82
|
+
else:
|
|
83
|
+
console.print(f"[green]✔ Created {issue.id} in status {issue.status}.[/green]")
|
|
84
|
+
console.print(f"Path: {rel_path}")
|
|
71
85
|
|
|
72
86
|
except ValueError as e:
|
|
73
87
|
OutputManager.error(str(e))
|
|
@@ -77,8 +91,8 @@ def create(
|
|
|
77
91
|
def update(
|
|
78
92
|
issue_id: str = typer.Argument(..., help="Issue ID to update"),
|
|
79
93
|
title: Optional[str] = typer.Option(None, "--title", "-t", help="New title"),
|
|
80
|
-
status: Optional[
|
|
81
|
-
stage: Optional[
|
|
94
|
+
status: Optional[str] = typer.Option(None, "--status", help="New status"),
|
|
95
|
+
stage: Optional[str] = typer.Option(None, "--stage", help="New stage"),
|
|
82
96
|
parent: Optional[str] = typer.Option(None, "--parent", "-p", help="Parent Issue ID"),
|
|
83
97
|
sprint: Optional[str] = typer.Option(None, "--sprint", help="Sprint ID"),
|
|
84
98
|
dependencies: Optional[List[str]] = typer.Option(None, "--dependency", "-d", help="Issue dependency ID(s)"),
|
|
@@ -125,7 +139,7 @@ def move_open(
|
|
|
125
139
|
issues_root = _resolve_issues_root(config, root)
|
|
126
140
|
try:
|
|
127
141
|
# Pull operation: Force stage to TODO
|
|
128
|
-
issue = core.update_issue(issues_root, issue_id, status=
|
|
142
|
+
issue = core.update_issue(issues_root, issue_id, status="open", stage="draft")
|
|
129
143
|
OutputManager.print({
|
|
130
144
|
"issue": issue,
|
|
131
145
|
"status": "opened"
|
|
@@ -153,13 +167,13 @@ def start(
|
|
|
153
167
|
|
|
154
168
|
try:
|
|
155
169
|
# Implicitly ensure status is Open
|
|
156
|
-
issue = core.update_issue(issues_root, issue_id, status=
|
|
170
|
+
issue = core.update_issue(issues_root, issue_id, status="open", stage="doing")
|
|
157
171
|
|
|
158
172
|
isolation_info = None
|
|
159
173
|
|
|
160
174
|
if branch:
|
|
161
175
|
try:
|
|
162
|
-
issue = core.start_issue_isolation(issues_root, issue_id,
|
|
176
|
+
issue = core.start_issue_isolation(issues_root, issue_id, "branch", project_root)
|
|
163
177
|
isolation_info = {"type": "branch", "ref": issue.isolation.ref}
|
|
164
178
|
except Exception as e:
|
|
165
179
|
OutputManager.error(f"Failed to create branch: {e}")
|
|
@@ -167,7 +181,7 @@ def start(
|
|
|
167
181
|
|
|
168
182
|
if worktree:
|
|
169
183
|
try:
|
|
170
|
-
issue = core.start_issue_isolation(issues_root, issue_id,
|
|
184
|
+
issue = core.start_issue_isolation(issues_root, issue_id, "worktree", project_root)
|
|
171
185
|
isolation_info = {"type": "worktree", "path": issue.isolation.path, "ref": issue.isolation.ref}
|
|
172
186
|
except Exception as e:
|
|
173
187
|
OutputManager.error(f"Failed to create worktree: {e}")
|
|
@@ -196,7 +210,7 @@ def submit(
|
|
|
196
210
|
project_root = _resolve_project_root(config)
|
|
197
211
|
try:
|
|
198
212
|
# Implicitly ensure status is Open
|
|
199
|
-
issue = core.update_issue(issues_root, issue_id, status=
|
|
213
|
+
issue = core.update_issue(issues_root, issue_id, status="open", stage="review")
|
|
200
214
|
|
|
201
215
|
# Delivery Report Generation
|
|
202
216
|
report_status = "skipped"
|
|
@@ -228,7 +242,7 @@ def submit(
|
|
|
228
242
|
@app.command("close")
|
|
229
243
|
def move_close(
|
|
230
244
|
issue_id: str = typer.Argument(..., help="Issue ID to close"),
|
|
231
|
-
solution: Optional[
|
|
245
|
+
solution: Optional[str] = typer.Option(None, "--solution", "-s", help="Solution type"),
|
|
232
246
|
prune: bool = typer.Option(False, "--prune", help="Delete branch/worktree after close"),
|
|
233
247
|
force: bool = typer.Option(False, "--force", help="Force delete branch/worktree"),
|
|
234
248
|
root: Optional[str] = typer.Option(None, "--root", help="Override issues root directory"),
|
|
@@ -241,12 +255,15 @@ def move_close(
|
|
|
241
255
|
|
|
242
256
|
# Pre-flight check for interactive guidance (Requirement FEAT-0082 #6)
|
|
243
257
|
if solution is None:
|
|
244
|
-
|
|
258
|
+
# Resolve options from engine
|
|
259
|
+
from .engine import get_engine
|
|
260
|
+
engine = get_engine(str(issues_root.parent))
|
|
261
|
+
valid_solutions = engine.issue_config.solutions or []
|
|
245
262
|
OutputManager.error(f"Closing an issue requires a solution. Options: {', '.join(valid_solutions)}")
|
|
246
263
|
raise typer.Exit(code=1)
|
|
247
264
|
|
|
248
265
|
try:
|
|
249
|
-
issue = core.update_issue(issues_root, issue_id, status=
|
|
266
|
+
issue = core.update_issue(issues_root, issue_id, status="closed", solution=solution)
|
|
250
267
|
|
|
251
268
|
pruned_resources = []
|
|
252
269
|
if prune:
|
|
@@ -276,7 +293,7 @@ def push(
|
|
|
276
293
|
config = get_config()
|
|
277
294
|
issues_root = _resolve_issues_root(config, root)
|
|
278
295
|
try:
|
|
279
|
-
issue = core.update_issue(issues_root, issue_id, status=
|
|
296
|
+
issue = core.update_issue(issues_root, issue_id, status="backlog")
|
|
280
297
|
OutputManager.print({
|
|
281
298
|
"issue": issue,
|
|
282
299
|
"status": "pushed_to_backlog"
|
|
@@ -295,7 +312,7 @@ def pull(
|
|
|
295
312
|
config = get_config()
|
|
296
313
|
issues_root = _resolve_issues_root(config, root)
|
|
297
314
|
try:
|
|
298
|
-
issue = core.update_issue(issues_root, issue_id, status=
|
|
315
|
+
issue = core.update_issue(issues_root, issue_id, status="open", stage="draft")
|
|
299
316
|
OutputManager.print({
|
|
300
317
|
"issue": issue,
|
|
301
318
|
"status": "pulled_from_backlog"
|
|
@@ -314,7 +331,7 @@ def cancel(
|
|
|
314
331
|
config = get_config()
|
|
315
332
|
issues_root = _resolve_issues_root(config, root)
|
|
316
333
|
try:
|
|
317
|
-
issue = core.update_issue(issues_root, issue_id, status=
|
|
334
|
+
issue = core.update_issue(issues_root, issue_id, status="closed", solution="cancelled")
|
|
318
335
|
OutputManager.print({
|
|
319
336
|
"issue": issue,
|
|
320
337
|
"status": "cancelled"
|
|
@@ -427,10 +444,10 @@ def board(
|
|
|
427
444
|
issue_list = []
|
|
428
445
|
for issue in sorted(issues, key=lambda x: x.updated_at, reverse=True):
|
|
429
446
|
type_color = {
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
447
|
+
"feature": "green",
|
|
448
|
+
"chore": "blue",
|
|
449
|
+
"fix": "red",
|
|
450
|
+
"epic": "magenta"
|
|
434
451
|
}.get(issue.type, "white")
|
|
435
452
|
|
|
436
453
|
issue_list.append(
|
|
@@ -458,8 +475,8 @@ def board(
|
|
|
458
475
|
@app.command("list")
|
|
459
476
|
def list_cmd(
|
|
460
477
|
status: Optional[str] = typer.Option(None, "--status", "-s", help="Filter by status (open, closed, backlog, all)"),
|
|
461
|
-
type: Optional[
|
|
462
|
-
stage: Optional[
|
|
478
|
+
type: Optional[str] = typer.Option(None, "--type", "-t", help="Filter by type"),
|
|
479
|
+
stage: Optional[str] = typer.Option(None, "--stage", help="Filter by stage"),
|
|
463
480
|
root: Optional[str] = typer.Option(None, "--root", help="Override issues root directory"),
|
|
464
481
|
workspace: bool = typer.Option(False, "--workspace", "-w", help="Include issues from workspace members"),
|
|
465
482
|
json: AgentOutput = False,
|
|
@@ -481,7 +498,7 @@ def list_cmd(
|
|
|
481
498
|
for i in issues:
|
|
482
499
|
# Status Filter
|
|
483
500
|
if target_status != "all":
|
|
484
|
-
if i.status
|
|
501
|
+
if i.status != target_status:
|
|
485
502
|
continue
|
|
486
503
|
|
|
487
504
|
# Type Filter
|
|
@@ -530,13 +547,13 @@ def _render_issues_table(issues: List[IssueMetadata], title: str = "Issues"):
|
|
|
530
547
|
t_color = type_colors.get(i.type, "white")
|
|
531
548
|
s_color = status_colors.get(i.status, "white")
|
|
532
549
|
|
|
533
|
-
stage_str = i.stage
|
|
550
|
+
stage_str = i.stage if i.stage else "-"
|
|
534
551
|
updated_str = i.updated_at.strftime("%Y-%m-%d %H:%M")
|
|
535
552
|
|
|
536
553
|
table.add_row(
|
|
537
554
|
i.id,
|
|
538
|
-
f"[{t_color}]{i.type
|
|
539
|
-
f"[{s_color}]{i.status
|
|
555
|
+
f"[{t_color}]{i.type}[/{t_color}]",
|
|
556
|
+
f"[{s_color}]{i.status}[/{s_color}]",
|
|
540
557
|
stage_str,
|
|
541
558
|
i.title,
|
|
542
559
|
updated_str
|
|
@@ -605,11 +622,11 @@ def scope(
|
|
|
605
622
|
return
|
|
606
623
|
|
|
607
624
|
tree = Tree(f"[bold blue]Monoco Issue Scope[/bold blue]")
|
|
608
|
-
epics = sorted([i for i in issues if i.type ==
|
|
609
|
-
stories = [i for i in issues if i.type ==
|
|
610
|
-
tasks = [i for i in issues if i.type in [
|
|
625
|
+
epics = sorted([i for i in issues if i.type == "epic"], key=lambda x: x.id)
|
|
626
|
+
stories = [i for i in issues if i.type == "feature"]
|
|
627
|
+
tasks = [i for i in issues if i.type in ["chore", "fix"]]
|
|
611
628
|
|
|
612
|
-
status_map = {
|
|
629
|
+
status_map = {"open": "[blue]●[/blue]", "closed": "[green]✔[/green]", "backlog": "[dim]💤[/dim]"}
|
|
613
630
|
|
|
614
631
|
for epic in epics:
|
|
615
632
|
epic_node = tree.add(f"{status_map[epic.status]} [bold]{epic.id}[/bold]: {epic.title}")
|
|
@@ -622,6 +639,94 @@ def scope(
|
|
|
622
639
|
|
|
623
640
|
console.print(Panel(tree, expand=False))
|
|
624
641
|
|
|
642
|
+
@app.command("sync-files")
|
|
643
|
+
def sync_files(
|
|
644
|
+
issue_id: Optional[str] = typer.Argument(None, help="Issue ID to sync (default: current context)"),
|
|
645
|
+
root: Optional[str] = typer.Option(None, "--root", help="Override issues root directory"),
|
|
646
|
+
json: AgentOutput = False,
|
|
647
|
+
):
|
|
648
|
+
"""
|
|
649
|
+
Sync issue 'files' field with git changed files.
|
|
650
|
+
"""
|
|
651
|
+
config = get_config()
|
|
652
|
+
issues_root = _resolve_issues_root(config, root)
|
|
653
|
+
project_root = _resolve_project_root(config)
|
|
654
|
+
|
|
655
|
+
if not issue_id:
|
|
656
|
+
# Infer from branch
|
|
657
|
+
from monoco.core import git
|
|
658
|
+
current = git.get_current_branch(project_root)
|
|
659
|
+
# Try to parse ID from branch "feat/issue-123-slug"
|
|
660
|
+
import re
|
|
661
|
+
match = re.search(r"(?:feat|fix|chore|epic)/([a-zA-Z]+-\d+)", current)
|
|
662
|
+
if match:
|
|
663
|
+
issue_id = match.group(1).upper()
|
|
664
|
+
else:
|
|
665
|
+
OutputManager.error("Cannot infer Issue ID from current branch. Please specify Issue ID.")
|
|
666
|
+
raise typer.Exit(code=1)
|
|
667
|
+
|
|
668
|
+
try:
|
|
669
|
+
changed = core.sync_issue_files(issues_root, issue_id, project_root)
|
|
670
|
+
OutputManager.print({
|
|
671
|
+
"id": issue_id,
|
|
672
|
+
"status": "synced",
|
|
673
|
+
"files": changed
|
|
674
|
+
})
|
|
675
|
+
except Exception as e:
|
|
676
|
+
OutputManager.error(str(e))
|
|
677
|
+
raise typer.Exit(code=1)
|
|
678
|
+
|
|
679
|
+
@app.command("inspect")
|
|
680
|
+
def inspect(
|
|
681
|
+
target: str = typer.Argument(..., help="Issue ID or File Path"),
|
|
682
|
+
root: Optional[str] = typer.Option(None, "--root", help="Override issues root directory"),
|
|
683
|
+
ast: bool = typer.Option(False, "--ast", help="Output JSON AST structure for debugging"),
|
|
684
|
+
json: AgentOutput = False,
|
|
685
|
+
):
|
|
686
|
+
"""
|
|
687
|
+
Inspect a specific issue and return its metadata (including actions).
|
|
688
|
+
"""
|
|
689
|
+
config = get_config()
|
|
690
|
+
issues_root = _resolve_issues_root(config, root)
|
|
691
|
+
|
|
692
|
+
# Try as Path
|
|
693
|
+
target_path = Path(target)
|
|
694
|
+
if target_path.exists() and target_path.is_file():
|
|
695
|
+
path = target_path
|
|
696
|
+
else:
|
|
697
|
+
# Try as ID
|
|
698
|
+
# Search path logic is needed? Or core.find_issue_path
|
|
699
|
+
path = core.find_issue_path(issues_root, target)
|
|
700
|
+
if not path:
|
|
701
|
+
OutputManager.error(f"Issue or file {target} not found.")
|
|
702
|
+
raise typer.Exit(code=1)
|
|
703
|
+
|
|
704
|
+
# AST Debug Mode
|
|
705
|
+
if ast:
|
|
706
|
+
from .domain.parser import MarkdownParser
|
|
707
|
+
content = path.read_text()
|
|
708
|
+
try:
|
|
709
|
+
domain_issue = MarkdownParser.parse(content, path=str(path))
|
|
710
|
+
print(domain_issue.model_dump_json(indent=2))
|
|
711
|
+
except Exception as e:
|
|
712
|
+
OutputManager.error(f"Failed to parse AST: {e}")
|
|
713
|
+
raise typer.Exit(code=1)
|
|
714
|
+
return
|
|
715
|
+
|
|
716
|
+
# Normal Mode
|
|
717
|
+
meta = core.parse_issue(path)
|
|
718
|
+
|
|
719
|
+
if not meta:
|
|
720
|
+
OutputManager.error(f"Could not parse issue {target}.")
|
|
721
|
+
raise typer.Exit(code=1)
|
|
722
|
+
|
|
723
|
+
# In JSON mode (AgentOutput), we might want to return rich data
|
|
724
|
+
if OutputManager.is_agent_mode():
|
|
725
|
+
OutputManager.print(meta)
|
|
726
|
+
else:
|
|
727
|
+
# For human, print yaml-like or table
|
|
728
|
+
console.print(meta)
|
|
729
|
+
|
|
625
730
|
@app.command("lint")
|
|
626
731
|
def lint(
|
|
627
732
|
recursive: bool = typer.Option(False, "--recursive", "-r", help="Recursively scan subdirectories"),
|
|
@@ -662,27 +767,9 @@ def _resolve_issues_root(config, cli_root: Optional[str]) -> Path:
|
|
|
662
767
|
return path
|
|
663
768
|
|
|
664
769
|
# 2. Handle Default / Contextual Execution (No --root)
|
|
665
|
-
#
|
|
770
|
+
# Strict Workspace Check: If not in a project root, we rely on the config root.
|
|
771
|
+
# (The global app callback already enforces presence of .monoco for most commands)
|
|
666
772
|
cwd = Path.cwd()
|
|
667
|
-
|
|
668
|
-
# If CWD is NOT a project root (no .monoco/), scan for subprojects
|
|
669
|
-
if not is_project_root(cwd):
|
|
670
|
-
subprojects = find_projects(cwd)
|
|
671
|
-
if len(subprojects) > 1:
|
|
672
|
-
console.print(f"[yellow]Workspace detected with {len(subprojects)} projects:[/yellow]")
|
|
673
|
-
for p in subprojects:
|
|
674
|
-
console.print(f" - [bold]{p.name}[/bold]")
|
|
675
|
-
console.print("\n[yellow]Please specify a project using --root <PATH>.[/yellow]")
|
|
676
|
-
# We don't exit here strictly, but usually this means we can't find 'Issues' in CWD anyway
|
|
677
|
-
# so the config fallbacks below will likely fail or point to non-existent CWD/Issues.
|
|
678
|
-
# But let's fail fast to be helpful.
|
|
679
|
-
raise typer.Exit(code=1)
|
|
680
|
-
elif len(subprojects) == 1:
|
|
681
|
-
# Auto-select the only child project?
|
|
682
|
-
# It's safer to require explicit intent, but let's try to be helpful if it's obvious.
|
|
683
|
-
# However, standard behavior is usually "operate on current dir".
|
|
684
|
-
# Let's stick to standard config resolution, but maybe warn.
|
|
685
|
-
pass
|
|
686
773
|
|
|
687
774
|
# 3. Config Fallback
|
|
688
775
|
config_issues_path = Path(config.paths.issues)
|
|
@@ -826,3 +913,40 @@ def commit(
|
|
|
826
913
|
except Exception as e:
|
|
827
914
|
console.print(f"[red]Git Error:[/red] {e}")
|
|
828
915
|
raise typer.Exit(code=1)
|
|
916
|
+
|
|
917
|
+
@lsp_app.command("definition")
|
|
918
|
+
def lsp_definition(
|
|
919
|
+
file: str = typer.Option(..., "--file", "-f", help="Abs path to file"),
|
|
920
|
+
line: int = typer.Option(..., "--line", "-l", help="0-indexed line number"),
|
|
921
|
+
character: int = typer.Option(..., "--char", "-c", help="0-indexed character number"),
|
|
922
|
+
):
|
|
923
|
+
"""
|
|
924
|
+
Handle textDocument/definition request.
|
|
925
|
+
Output: JSON Location | null
|
|
926
|
+
"""
|
|
927
|
+
import json
|
|
928
|
+
from monoco.core.lsp import Position
|
|
929
|
+
from monoco.features.issue.lsp import DefinitionProvider
|
|
930
|
+
|
|
931
|
+
config = get_config()
|
|
932
|
+
# Workspace Root resolution is key here.
|
|
933
|
+
# If we are in a workspace, we want the workspace root, not just issue root.
|
|
934
|
+
# _resolve_project_root returns the closest project root or monoco root.
|
|
935
|
+
workspace_root = _resolve_project_root(config)
|
|
936
|
+
# Search for topmost workspace root to enable cross-project navigation
|
|
937
|
+
current_best = workspace_root
|
|
938
|
+
for parent in [workspace_root] + list(workspace_root.parents):
|
|
939
|
+
if (parent / ".monoco" / "workspace.yaml").exists() or (parent / ".monoco" / "project.yaml").exists():
|
|
940
|
+
current_best = parent
|
|
941
|
+
workspace_root = current_best
|
|
942
|
+
|
|
943
|
+
provider = DefinitionProvider(workspace_root)
|
|
944
|
+
file_path = Path(file)
|
|
945
|
+
|
|
946
|
+
locations = provider.provide_definition(
|
|
947
|
+
file_path,
|
|
948
|
+
Position(line=line, character=character)
|
|
949
|
+
)
|
|
950
|
+
|
|
951
|
+
# helper to serialize
|
|
952
|
+
print(json.dumps([l.model_dump(mode='json') for l in locations]))
|