monoco-toolkit 0.1.1__py3-none-any.whl → 0.2.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. monoco/cli/__init__.py +0 -0
  2. monoco/cli/project.py +87 -0
  3. monoco/cli/workspace.py +46 -0
  4. monoco/core/agent/__init__.py +5 -0
  5. monoco/core/agent/action.py +144 -0
  6. monoco/core/agent/adapters.py +106 -0
  7. monoco/core/agent/protocol.py +31 -0
  8. monoco/core/agent/state.py +106 -0
  9. monoco/core/config.py +152 -17
  10. monoco/core/execution.py +62 -0
  11. monoco/core/feature.py +58 -0
  12. monoco/core/git.py +51 -2
  13. monoco/core/injection.py +196 -0
  14. monoco/core/integrations.py +234 -0
  15. monoco/core/lsp.py +61 -0
  16. monoco/core/output.py +13 -2
  17. monoco/core/registry.py +36 -0
  18. monoco/core/resources/en/AGENTS.md +8 -0
  19. monoco/core/resources/en/SKILL.md +66 -0
  20. monoco/core/resources/zh/AGENTS.md +8 -0
  21. monoco/core/resources/zh/SKILL.md +66 -0
  22. monoco/core/setup.py +88 -110
  23. monoco/core/skills.py +444 -0
  24. monoco/core/state.py +53 -0
  25. monoco/core/sync.py +224 -0
  26. monoco/core/telemetry.py +4 -1
  27. monoco/core/workspace.py +85 -20
  28. monoco/daemon/app.py +127 -58
  29. monoco/daemon/models.py +4 -0
  30. monoco/daemon/services.py +56 -155
  31. monoco/features/agent/commands.py +166 -0
  32. monoco/features/agent/doctor.py +30 -0
  33. monoco/features/config/commands.py +125 -44
  34. monoco/features/i18n/adapter.py +29 -0
  35. monoco/features/i18n/commands.py +89 -10
  36. monoco/features/i18n/core.py +113 -27
  37. monoco/features/i18n/resources/en/AGENTS.md +8 -0
  38. monoco/features/i18n/resources/en/SKILL.md +94 -0
  39. monoco/features/i18n/resources/zh/AGENTS.md +8 -0
  40. monoco/features/i18n/resources/zh/SKILL.md +94 -0
  41. monoco/features/issue/adapter.py +34 -0
  42. monoco/features/issue/commands.py +183 -65
  43. monoco/features/issue/core.py +172 -77
  44. monoco/features/issue/linter.py +215 -116
  45. monoco/features/issue/migration.py +134 -0
  46. monoco/features/issue/models.py +23 -19
  47. monoco/features/issue/monitor.py +94 -0
  48. monoco/features/issue/resources/en/AGENTS.md +15 -0
  49. monoco/features/issue/resources/en/SKILL.md +87 -0
  50. monoco/features/issue/resources/zh/AGENTS.md +15 -0
  51. monoco/features/issue/resources/zh/SKILL.md +114 -0
  52. monoco/features/issue/validator.py +269 -0
  53. monoco/features/pty/core.py +185 -0
  54. monoco/features/pty/router.py +138 -0
  55. monoco/features/pty/server.py +56 -0
  56. monoco/features/spike/adapter.py +30 -0
  57. monoco/features/spike/commands.py +45 -24
  58. monoco/features/spike/core.py +4 -21
  59. monoco/features/spike/resources/en/AGENTS.md +7 -0
  60. monoco/features/spike/resources/en/SKILL.md +74 -0
  61. monoco/features/spike/resources/zh/AGENTS.md +7 -0
  62. monoco/features/spike/resources/zh/SKILL.md +74 -0
  63. monoco/main.py +115 -2
  64. {monoco_toolkit-0.1.1.dist-info → monoco_toolkit-0.2.5.dist-info}/METADATA +2 -2
  65. monoco_toolkit-0.2.5.dist-info/RECORD +77 -0
  66. monoco_toolkit-0.1.1.dist-info/RECORD +0 -33
  67. {monoco_toolkit-0.1.1.dist-info → monoco_toolkit-0.2.5.dist-info}/WHEEL +0 -0
  68. {monoco_toolkit-0.1.1.dist-info → monoco_toolkit-0.2.5.dist-info}/entry_points.txt +0 -0
  69. {monoco_toolkit-0.1.1.dist-info → monoco_toolkit-0.2.5.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,138 @@
1
+
2
+ from fastapi import APIRouter, WebSocket, WebSocketDisconnect, Query
3
+ from pydantic import BaseModel
4
+ from typing import Optional, Dict
5
+ import json
6
+ import asyncio
7
+ import logging
8
+ import os
9
+ from pathlib import Path
10
+ from monoco.features.pty.core import PTYManager
11
+ from monoco.core.config import get_config
12
+
13
+ # We will use dependency injection or a global singleton for now
14
+ # Ideally attached to app state
15
+ pty_manager = PTYManager()
16
+
17
+ router = APIRouter(prefix="/api/v1/pty", tags=["pty"])
18
+
19
+ logger = logging.getLogger("monoco.pty")
20
+
21
+ @router.websocket("/ws/{session_id}")
22
+ async def websocket_pty_endpoint(
23
+ websocket: WebSocket,
24
+ session_id: str,
25
+ cwd: Optional[str] = Query(None),
26
+ cols: int = Query(80),
27
+ rows: int = Query(24),
28
+ env: Optional[str] = Query(None) # JSON-encoded env vars
29
+ ):
30
+ await websocket.accept()
31
+
32
+ # Determine working directory
33
+ # 1. Provide explicit CWD in query
34
+ # 2. Or fallback to ProjectRoot from env (if integrated)
35
+ # 3. Or fallback to process CWD
36
+
37
+ # Since monoco pty runs as a separate service, we expect CWD to be passed
38
+ # or we default to where monoco pty was started
39
+ working_dir = cwd if cwd else os.getcwd()
40
+
41
+ # Prepare environment
42
+ env_vars = os.environ.copy()
43
+ env_vars["TERM"] = "xterm-256color"
44
+ env_vars["COLORTERM"] = "truecolor"
45
+ if "SHELL" not in env_vars:
46
+ env_vars["SHELL"] = "/bin/zsh"
47
+ if "HOME" not in env_vars:
48
+ import pathlib
49
+ env_vars["HOME"] = str(pathlib.Path.home())
50
+
51
+ # Filter out Trae/Gemini specific variables to avoid shell integration conflicts
52
+ # This prevents the shell from trying to write to IDE-specific logs which causes EPERM
53
+ keys_to_remove = [k for k in env_vars.keys() if k.startswith("TRAE_") or k.startswith("GEMINI_") or k == "AI_AGENT"]
54
+ for k in keys_to_remove:
55
+ del env_vars[k]
56
+
57
+ if env:
58
+ try:
59
+ custom_env = json.loads(env)
60
+ env_vars.update(custom_env)
61
+ except:
62
+ logger.warning("Failed to parse custom env vars")
63
+
64
+ # Start Session
65
+ try:
66
+ session = pty_manager.create_session(
67
+ session_id=session_id,
68
+ cwd=working_dir,
69
+ cmd=["/bin/zsh", "-l"], # Use login shell to ensure full user environment
70
+ env=env_vars
71
+ )
72
+ session.start(cols, rows)
73
+ except Exception as e:
74
+ logger.error(f"Failed to start session: {e}")
75
+ await websocket.close(code=1011)
76
+ return
77
+
78
+ # Pipe Loop
79
+ reader_task = None
80
+ try:
81
+ # Task to read from PTY and send to WebSocket
82
+ async def pty_reader():
83
+ while session.running:
84
+ data = await session.read()
85
+ if not data:
86
+ break
87
+ # xterm.js expects string or binary. We send string/bytes.
88
+ # Usually text is fine, but binary is safer for control codes.
89
+ await websocket.send_bytes(data)
90
+
91
+ # If PTY exits, close WS
92
+ await websocket.close()
93
+
94
+ reader_task = asyncio.create_task(pty_reader())
95
+
96
+ # Main loop: Read from WebSocket and write to PTY
97
+ try:
98
+ while True:
99
+ # Receive message from Client (xterm.js)
100
+ # Message can be simple input string, or a JSON command (resize)
101
+ message = await websocket.receive()
102
+
103
+ if message["type"] == "websocket.disconnect":
104
+ raise WebSocketDisconnect(code=message.get("code", 1000))
105
+
106
+ if "text" in message:
107
+ payload = message["text"]
108
+
109
+ # Check if it's a control message (Hack: usually client sends raw input)
110
+ # We can enforce a protocol: binary for Input, text JSON for Control.
111
+ try:
112
+ # Try parsing as JSON control message
113
+ cmd = json.loads(payload)
114
+ if cmd.get("type") == "resize":
115
+ session.resize(cmd["cols"], cmd["rows"])
116
+ continue
117
+ except:
118
+ pass # Not JSON, treat as raw input
119
+
120
+ session.write(payload.encode())
121
+
122
+ elif "bytes" in message:
123
+ session.write(message["bytes"])
124
+ except RuntimeError:
125
+ # Handle "Cannot call 'receive' once a disconnect message has been received"
126
+ # This happens if Starlette/FastAPI already processed the disconnect internally
127
+ # but we called receive() again.
128
+ logger.info(f"Runtime disconnect for session {session_id}")
129
+
130
+ except WebSocketDisconnect:
131
+ logger.info(f"Client disconnected for session {session_id}")
132
+ except Exception as e:
133
+ logger.error(f"WebSocket error: {e}")
134
+ finally:
135
+ # Cleanup
136
+ pty_manager.close_session(session_id)
137
+ if reader_task and not reader_task.done():
138
+ reader_task.cancel()
@@ -0,0 +1,56 @@
1
+ import logging
2
+ import signal
3
+ import sys
4
+ from typing import Optional
5
+ from pathlib import Path
6
+ from contextlib import asynccontextmanager
7
+ import uvicorn
8
+ from fastapi import FastAPI
9
+ from monoco.features.pty.router import router as pty_router, pty_manager
10
+
11
+ @asynccontextmanager
12
+ async def lifespan(app: FastAPI):
13
+ # Startup
14
+ yield
15
+ # Shutdown
16
+ logging.info("Shutting down PTY manager and cleaning up sessions...")
17
+ pty_manager.close_all_sessions()
18
+
19
+ def run_pty_server(host: str = "127.0.0.1", port: int = 3124, cwd: Optional[Path] = None):
20
+ """
21
+ Entry point for the 'monoco pty' command.
22
+ """
23
+ # Configure Logging
24
+ logging.basicConfig(level=logging.INFO)
25
+
26
+ # Register a manual signal handler to ensure we catch termination even if uvicorn misses it
27
+ # or if we are stuck before uvicorn starts.
28
+ def handle_signal(signum, frame):
29
+ logging.info(f"Received signal {signum}, initiating shutdown...")
30
+ # We rely on uvicorn to handle the actual exit loop for SIGINT/SIGTERM usually,
31
+ # but having this log confirms propagation.
32
+ # If uvicorn is running, it should catch this first.
33
+ # If not, we exit manually.
34
+ sys.exit(0)
35
+
36
+ # Note: Uvicorn overwrites SIGINT/SIGTERM handlers by default.
37
+ # relying on lifespan is the standard "Uvicorn way".
38
+
39
+ app = FastAPI(title="Monoco PTY Service", lifespan=lifespan)
40
+ app.include_router(pty_router)
41
+
42
+ # If cwd is provided, we might want to set it as current process CWD
43
+ # so that new sessions default to it.
44
+ if cwd and cwd.exists():
45
+ import os
46
+ os.chdir(cwd)
47
+ logging.info(f"PTY Service Root: {cwd}")
48
+
49
+ logging.info(f"Starting Monoco PTY Service on ws://{host}:{port}")
50
+ try:
51
+ uvicorn.run(app, host=host, port=port)
52
+ except KeyboardInterrupt:
53
+ pass
54
+ finally:
55
+ # Final safety net
56
+ pty_manager.close_all_sessions()
@@ -0,0 +1,30 @@
1
+ from pathlib import Path
2
+ from typing import Dict
3
+ from monoco.core.feature import MonocoFeature, IntegrationData
4
+ from monoco.features.spike import core
5
+
6
+ class SpikeFeature(MonocoFeature):
7
+ @property
8
+ def name(self) -> str:
9
+ return "spike"
10
+
11
+ def initialize(self, root: Path, config: Dict) -> None:
12
+ spikes_name = config.get("paths", {}).get("spikes", ".references")
13
+ core.init(root, spikes_name)
14
+
15
+ def integrate(self, root: Path, config: Dict) -> IntegrationData:
16
+ # Determine language from config, default to 'en'
17
+ lang = config.get("i18n", {}).get("source_lang", "en")
18
+ base_dir = Path(__file__).parent / "resources"
19
+
20
+ prompt_file = base_dir / lang / "AGENTS.md"
21
+ if not prompt_file.exists():
22
+ prompt_file = base_dir / "en" / "AGENTS.md"
23
+
24
+ content = ""
25
+ if prompt_file.exists():
26
+ content = prompt_file.read_text(encoding="utf-8").strip()
27
+
28
+ return IntegrationData(
29
+ system_prompts={"Spike (Research)": content}
30
+ )
@@ -1,15 +1,18 @@
1
1
  import typer
2
2
  from pathlib import Path
3
3
  from rich.console import Console
4
+ from typing import Annotated
4
5
 
5
6
  from monoco.core.config import get_config
7
+ from monoco.core.output import AgentOutput, OutputManager
6
8
  from . import core
7
9
 
8
10
  app = typer.Typer(help="Spike & Repo Management.")
9
- console = Console()
10
11
 
11
12
  @app.command("init")
12
- def init():
13
+ def init(
14
+ json: AgentOutput = False,
15
+ ):
13
16
  """Initialize the Spike environment (gitignore setup)."""
14
17
  config = get_config()
15
18
  root_dir = Path(config.paths.root)
@@ -20,11 +23,16 @@ def init():
20
23
  # Create the directory
21
24
  (root_dir / spikes_dir_name).mkdir(exist_ok=True)
22
25
 
23
- console.print(f"[green]✔[/green] Initialized Spike environment. Added '{spikes_dir_name}/' to .gitignore.")
26
+ OutputManager.print({
27
+ "status": "initialized",
28
+ "directory": spikes_dir_name,
29
+ "gitignore_updated": True
30
+ })
24
31
 
25
32
  @app.command("add")
26
33
  def add_repo(
27
34
  url: str = typer.Argument(..., help="Git Repository URL"),
35
+ json: AgentOutput = False,
28
36
  ):
29
37
  """Add a new research repository."""
30
38
  config = get_config()
@@ -38,13 +46,18 @@ def add_repo(
38
46
  name = name[:-4]
39
47
 
40
48
  core.update_config_repos(root_dir, name, url)
41
- console.print(f"[green]✔[/green] Added repo [bold]{name}[/bold] ({url}) to configuration.")
42
- console.print("Run [bold]monoco spike sync[/bold] to download content.")
49
+ OutputManager.print({
50
+ "status": "added",
51
+ "name": name,
52
+ "url": url,
53
+ "message": f"Run 'monoco spike sync' to download content."
54
+ })
43
55
 
44
56
  @app.command("remove")
45
57
  def remove_repo(
46
58
  name: str = typer.Argument(..., help="Repository Name"),
47
59
  force: bool = typer.Option(False, "--force", "-f", help="Force delete physical directory without asking"),
60
+ json: AgentOutput = False,
48
61
  ):
49
62
  """Remove a repository from configuration."""
50
63
  config = get_config()
@@ -52,30 +65,34 @@ def remove_repo(
52
65
  spikes_dir = root_dir / config.paths.spikes
53
66
 
54
67
  if name not in config.project.spike_repos:
55
- console.print(f"[yellow]![/yellow] Repo [bold]{name}[/bold] not found in configuration.")
68
+ OutputManager.error(f"Repo {name} not found in configuration.")
56
69
  return
57
70
 
58
71
  # Remove from config
59
72
  core.update_config_repos(root_dir, name, "", remove=True)
60
- console.print(f"[green]✔[/green] Removed [bold]{name}[/bold] from configuration.")
61
73
 
62
74
  target_path = spikes_dir / name
75
+ deleted = False
63
76
  if target_path.exists():
64
77
  if force or typer.confirm(f"Do you want to delete the directory {target_path}?", default=False):
65
78
  core.remove_repo_dir(spikes_dir, name)
66
- console.print(f"[gray]✔[/gray] Deleted directory {target_path}.")
79
+ deleted = True
67
80
  else:
68
- console.print(f"[gray]ℹ[/gray] Directory {target_path} kept.")
81
+ deleted = False
82
+
83
+ OutputManager.print({
84
+ "status": "removed",
85
+ "name": name,
86
+ "directory_deleted": deleted
87
+ })
69
88
 
70
89
  @app.command("sync")
71
- def sync_repos():
90
+ def sync_repos(
91
+ json: AgentOutput = False,
92
+ ):
72
93
  """Sync (Clone/Pull) all configured repositories."""
73
94
  # Force reload config to get latest updates
74
95
  config = get_config()
75
- # Note: get_config is a singleton, so for 'add' then 'sync' in same process,
76
- # we rely on 'add' writing to disk and us reading from memory?
77
- # Actually, if we run standard CLI "monoco spike add" then "monoco spike sync",
78
- # they are separate processes, so config loads fresh.
79
96
 
80
97
  root_dir = Path(config.paths.root)
81
98
  spikes_dir = root_dir / config.paths.spikes
@@ -84,27 +101,31 @@ def sync_repos():
84
101
  repos = config.project.spike_repos
85
102
 
86
103
  if not repos:
87
- console.print("[yellow]No repositories configured.[/yellow] Use 'monoco spike add <url>' first.")
104
+ OutputManager.print({"status": "empty", "message": "No repositories configured."}, title="Sync")
88
105
  return
89
106
 
90
- console.print(f"Syncing {len(repos)} repositories...")
107
+ results = []
91
108
 
92
109
  for name, url in repos.items():
93
- core.sync_repo(root_dir, spikes_dir, name, url)
110
+ try:
111
+ core.sync_repo(root_dir, spikes_dir, name, url)
112
+ results.append({"name": name, "status": "synced", "url": url})
113
+ except Exception as e:
114
+ results.append({"name": name, "status": "failed", "error": str(e), "url": url})
94
115
 
95
- console.print("[green]✔[/green] Sync complete.")
116
+ OutputManager.print(results, title="Sync Results")
96
117
 
97
- # Alias for list (showing configured repos) could be useful but not strictly asked for.
98
- # Let's add a simple list command to see what we have.
99
118
  @app.command("list")
100
- def list_repos():
119
+ def list_repos(
120
+ json: AgentOutput = False,
121
+ ):
101
122
  """List configured repositories."""
102
123
  config = get_config()
103
124
  repos = config.project.spike_repos
104
125
 
105
126
  if not repos:
106
- console.print("[yellow]No repositories configured.[/yellow]")
127
+ OutputManager.print([], title="Repositories")
107
128
  return
108
129
 
109
- for name, url in repos.items():
110
- console.print(f"- [bold]{name}[/bold]: {url}")
130
+ data = [{"name": name, "url": url} for name, url in repos.items()]
131
+ OutputManager.print(data, title="Repositories")
@@ -31,17 +31,10 @@ def run_git_command(cmd: List[str], cwd: Path) -> bool:
31
31
 
32
32
  def get_config_file_path(root: Path) -> Path:
33
33
  """Determine the config file to update."""
34
- # Priority 1: .monoco/config.yaml
35
- hidden = root / ".monoco" / "config.yaml"
36
- if hidden.exists():
37
- return hidden
34
+ # Standard: .monoco/project.yaml
35
+ hidden = root / ".monoco" / "project.yaml"
38
36
 
39
- # Priority 2: monoco.yaml
40
- visible = root / "monoco.yaml"
41
- if visible.exists():
42
- return visible
43
-
44
- # Default to .monoco/config.yaml for new files
37
+ # Ensure parent exists
45
38
  hidden.parent.mkdir(exist_ok=True)
46
39
  return hidden
47
40
 
@@ -131,24 +124,14 @@ This skill normalizes how we introduce external code repositories.
131
124
  3. **Remove**: `monoco spike remove <name>`
132
125
  """
133
126
 
134
- PROMPT_CONTENT = """### Spike (Research)
135
- Manage external reference repositories.
136
- - **Add Repo**: `monoco spike add <url>` (Available in `.reference/<name>` for reading)
137
- - **Sync**: `monoco spike sync` (Run to download content)
138
- - **Constraint**: Never edit files in `.reference/`. Treat them as read-only external knowledge."""
139
-
140
127
  def init(root: Path, spikes_dir_name: str):
141
128
  """Initialize Spike environment."""
142
129
  ensure_gitignore(root, spikes_dir_name)
143
130
  (root / spikes_dir_name).mkdir(exist_ok=True)
144
131
 
145
- def get_resources() -> Dict[str, Any]:
146
132
  return {
147
133
  "skills": {
148
134
  "git-repo-spike": SKILL_CONTENT
149
135
  },
150
- "prompts": {
151
- "spike": PROMPT_CONTENT
152
- }
136
+ "prompts": {} # Handled by adapter via resource files
153
137
  }
154
-
@@ -0,0 +1,7 @@
1
+ ### Spike (Research)
2
+
3
+ Manage external reference repositories.
4
+
5
+ - **Add Repo**: `monoco spike add <url>` (Available in `.reference/<name>` for reading)
6
+ - **Sync**: `monoco spike sync` (Run to download content)
7
+ - **Constraint**: Never edit files in `.reference/`. Treat them as read-only external knowledge.
@@ -0,0 +1,74 @@
1
+ ---
2
+ name: monoco-spike
3
+ description: Manage external reference repositories for research and learning. Provides read-only access to curated codebases.
4
+ ---
5
+
6
+ # Spike (Research)
7
+
8
+ Manage external reference repositories in Monoco projects.
9
+
10
+ ## Overview
11
+
12
+ The Spike feature allows you to:
13
+
14
+ - **Add external repositories** as read-only references
15
+ - **Sync repository content** to local `.references/` directory
16
+ - **Access curated knowledge** without modifying source code
17
+
18
+ ## Key Commands
19
+
20
+ ### Add Repository
21
+
22
+ ```bash
23
+ monoco spike add <url>
24
+ ```
25
+
26
+ Adds an external repository as a reference. The repository will be cloned to `.references/<name>/` where `<name>` is derived from the repository URL.
27
+
28
+ **Example**:
29
+
30
+ ```bash
31
+ monoco spike add https://github.com/example/awesome-project
32
+ # Available at: .references/awesome-project/
33
+ ```
34
+
35
+ ### Sync Repositories
36
+
37
+ ```bash
38
+ monoco spike sync
39
+ ```
40
+
41
+ Downloads or updates all configured spike repositories from `.monoco/config.yaml`.
42
+
43
+ ### List Spikes
44
+
45
+ ```bash
46
+ monoco spike list
47
+ ```
48
+
49
+ Shows all configured spike repositories and their sync status.
50
+
51
+ ## Configuration
52
+
53
+ Spike repositories are configured in `.monoco/config.yaml`:
54
+
55
+ ```yaml
56
+ project:
57
+ spike_repos:
58
+ awesome-project: https://github.com/example/awesome-project
59
+ another-ref: https://github.com/example/another-ref
60
+ ```
61
+
62
+ ## Best Practices
63
+
64
+ 1. **Read-Only Access**: Never edit files in `.references/`. Treat them as external knowledge.
65
+ 2. **Curated Selection**: Only add high-quality, relevant repositories.
66
+ 3. **Regular Sync**: Run `monoco spike sync` periodically to get updates.
67
+ 4. **Commit Configuration**: Add spike repo URLs to version control for team consistency.
68
+
69
+ ## Use Cases
70
+
71
+ - **Learning from Examples**: Study well-architected codebases
72
+ - **API Reference**: Keep framework documentation locally
73
+ - **Pattern Library**: Maintain a collection of design patterns
74
+ - **Competitive Analysis**: Reference similar projects
@@ -0,0 +1,7 @@
1
+ ### Spike (研究)
2
+
3
+ 管理外部参考仓库。
4
+
5
+ - **添加仓库**: `monoco spike add <url>` (在 `.reference/<name>` 中可读)
6
+ - **同步**: `monoco spike sync` (运行以下载内容)
7
+ - **约束**: 永远不要编辑 `.reference/` 中的文件。将它们视为只读的外部知识。
@@ -0,0 +1,74 @@
1
+ ---
2
+ name: monoco-spike
3
+ description: 管理用于研究和学习的外部参考仓库。提供对精选代码库的只读访问。
4
+ ---
5
+
6
+ # Spike (研究)
7
+
8
+ 在 Monoco 项目中管理外部参考仓库。
9
+
10
+ ## 概述
11
+
12
+ Spike 功能允许你:
13
+
14
+ - **添加外部仓库**作为只读参考
15
+ - **同步仓库内容**到本地 `.references/` 目录
16
+ - **访问精选知识**而不修改源代码
17
+
18
+ ## 核心命令
19
+
20
+ ### 添加仓库
21
+
22
+ ```bash
23
+ monoco spike add <url>
24
+ ```
25
+
26
+ 将外部仓库添加为参考。仓库将被克隆到 `.references/<name>/`,其中 `<name>` 从仓库 URL 派生。
27
+
28
+ **示例**:
29
+
30
+ ```bash
31
+ monoco spike add https://github.com/example/awesome-project
32
+ # 可在以下位置访问: .references/awesome-project/
33
+ ```
34
+
35
+ ### 同步仓库
36
+
37
+ ```bash
38
+ monoco spike sync
39
+ ```
40
+
41
+ 从 `.monoco/config.yaml` 下载或更新所有配置的 spike 仓库。
42
+
43
+ ### 列出 Spikes
44
+
45
+ ```bash
46
+ monoco spike list
47
+ ```
48
+
49
+ 显示所有配置的 spike 仓库及其同步状态。
50
+
51
+ ## 配置
52
+
53
+ Spike 仓库在 `.monoco/config.yaml` 中配置:
54
+
55
+ ```yaml
56
+ project:
57
+ spike_repos:
58
+ awesome-project: https://github.com/example/awesome-project
59
+ another-ref: https://github.com/example/another-ref
60
+ ```
61
+
62
+ ## 最佳实践
63
+
64
+ 1. **只读访问**: 永远不要编辑 `.references/` 中的文件。将它们视为外部知识。
65
+ 2. **精选选择**: 只添加高质量、相关的仓库。
66
+ 3. **定期同步**: 定期运行 `monoco spike sync` 以获取更新。
67
+ 4. **提交配置**: 将 spike 仓库 URL 添加到版本控制以保持团队一致性。
68
+
69
+ ## 使用场景
70
+
71
+ - **从示例中学习**: 研究架构良好的代码库
72
+ - **API 参考**: 在本地保存框架文档
73
+ - **模式库**: 维护设计模式集合
74
+ - **竞品分析**: 参考类似项目