Reorganize repo: backend/, deploy/, docs/ layout + AGENTS.md
Restructure the monorepo into clear top-level directories: - backend/: Go module root (cmd, internal, pkg, configs, migrations, docs/swagger, scripts, tests, go.mod, Makefile, .air.toml) - deploy/: Docker (Dockerfile, docker-compose*), quickstart, fluentd - docs/: project documentation + reports/ (moved from repo root) - AGENTS.md: new AI coding-agent guide at repo root Update all references to the new layout: - Dockerfile: COPY backend/go.mod, COPY backend/ (context = repo root) - docker-compose files: context ../.., dockerfile deploy/docker/Dockerfile, env_file ../../.env, volume mounts ../../backend:/app - deploy/quickstart/compose.yaml: dockerfile deploy/docker/Dockerfile - CI: working-directory: backend for go commands, file deploy/docker/Dockerfile, coverage path backend/coverage.out, health_check backend/scripts/ - backend/Makefile: docker target uses -f ../deploy/docker/Dockerfile ../ - README: architecture tree, quickstart, config paths updated Move root stray scripts (rename_models.*, run_m11_tests.sh, verify_build.sh, gorm_bool_main.go) to backend/scripts/legacy/. All moves via git mv to preserve history. Build, vet, SQLite tests, and docker compose config verified.
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
import re, os, sys
|
||||
|
||||
# Parse Chatwoot routes.rb to extract all API routes
|
||||
routes_rb = '/home/yanghao05/Workspace/chatwoot-reference/config/routes.rb'
|
||||
with open(routes_rb) as f:
|
||||
text = f.read()
|
||||
|
||||
# We need to parse nested namespace/resource blocks
|
||||
# Strategy: manually parse the nested structure
|
||||
|
||||
# Known Chatwoot API route structure from routes.rb analysis:
|
||||
# - /api/v1/ namespace with nested resources
|
||||
# - /api/v2/ namespace
|
||||
# - /public/ namespace
|
||||
# - /widget/ namespace
|
||||
# - Webhook routes
|
||||
# - Auth routes
|
||||
|
||||
# Let's extract all controller#action pairs from the controllers directory
|
||||
# and map them to expected routes
|
||||
|
||||
controllers_dir = '/home/yanghao05/Workspace/chatwoot-reference/app/controllers/api'
|
||||
routes = []
|
||||
|
||||
for root, dirs, files in os.walk(controllers_dir):
|
||||
for fname in files:
|
||||
if fname.endswith('_controller.rb') and 'base_controller' not in fname and 'concerns' not in root:
|
||||
fpath = os.path.join(root, fname)
|
||||
# Convert file path to controller name
|
||||
rel_path = os.path.relpath(fpath, '/home/yanghao05/Workspace/chatwoot-reference/app/controllers')
|
||||
rel_path = rel_path.replace('.rb', '')
|
||||
# Convert path separators to :: for Ruby module notation
|
||||
controller_name = rel_path.replace('/', '::')
|
||||
|
||||
# Read the file to extract public action methods
|
||||
with open(fpath) as cf:
|
||||
controller_text = cf.read()
|
||||
|
||||
# Find all public method definitions (actions)
|
||||
# In Rails controllers, actions are public methods defined with def
|
||||
actions = []
|
||||
in_private = False
|
||||
in_protected = False
|
||||
for line in controller_text.split('\n'):
|
||||
stripped = line.strip()
|
||||
if stripped == 'private' or stripped.startswith('private :'):
|
||||
in_private = True
|
||||
continue
|
||||
if stripped == 'protected' or stripped.startswith('protected :'):
|
||||
in_protected = True
|
||||
continue
|
||||
if re.match(r'def\s+\w+', stripped) and not in_private and not in_protected:
|
||||
action_name = re.match(r'def\s+(\w+)', stripped).group(1)
|
||||
# Skip internal/helper methods
|
||||
if not action_name.startswith('_') and action_name not in ['initialize', 'setup', 'teardown']:
|
||||
actions.append(action_name)
|
||||
# Reset private/protected scope when encountering new visibility keyword
|
||||
if stripped == 'public':
|
||||
in_private = False
|
||||
in_protected = False
|
||||
|
||||
for action in actions:
|
||||
routes.append((controller_name, action))
|
||||
|
||||
# Now map controller names to API routes
|
||||
# Standard Rails routing conventions:
|
||||
# api/v1/accounts/conversations_controller -> /api/v1/accounts/:account_id/conversations
|
||||
# api/v1/accounts/conversations/messages_controller -> /api/v1/accounts/:account_id/conversations/:conversation_id/messages
|
||||
|
||||
# Build route path from controller name
|
||||
def controller_to_route_path(controller_name):
|
||||
"""Convert Ruby controller name to expected route path prefix."""
|
||||
parts = controller_name.split('::')
|
||||
path_parts = []
|
||||
resource_stack = []
|
||||
|
||||
for part in parts:
|
||||
# Skip 'api' and 'v1'/'v2' - they become path prefixes
|
||||
if part == 'api':
|
||||
continue
|
||||
if part in ('v1', 'v2'):
|
||||
path_parts.append(f'/api/{part}')
|
||||
continue
|
||||
|
||||
# Singularize: Rails uses plural for resources, singular for nested resource names
|
||||
# Check if this is a nested singular resource (like 'contacts' inside conversations)
|
||||
name = part
|
||||
|
||||
# Convert CamelCase to snake_case
|
||||
name = re.sub(r'([A-Z])', r'_\1', name).lower().lstrip('_')
|
||||
|
||||
# Handle special cases
|
||||
if name == 'captain':
|
||||
path_parts.append('/captain')
|
||||
continue
|
||||
|
||||
# This part represents a resource
|
||||
# If it's a plural resource name, it gets :id parameter in routes
|
||||
# Common plural resources
|
||||
plural_resources = [
|
||||
'accounts', 'conversations', 'contacts', 'inboxes', 'messages',
|
||||
'agents', 'teams', 'team_members', 'labels', 'notifications',
|
||||
'campaigns', 'automation_rules', 'canned_responses', 'webhooks',
|
||||
'portals', 'categories', 'articles', 'csat_survey_responses',
|
||||
'macros', 'custom_filters', 'custom_attribute_definitions',
|
||||
'agent_bots', 'dashboard_apps', 'assignment_policies',
|
||||
'sla_policies', 'custom_roles', 'whatsapp_calls',
|
||||
'bulk_actions', 'inbox_members', 'notes',
|
||||
'agent_capacity_policies', 'inbox_limits',
|
||||
'copilot_threads', 'copilot_messages'
|
||||
]
|
||||
|
||||
if name in plural_resources:
|
||||
path_parts.append(f'/{name}')
|
||||
path_parts.append(f'/:{"id" if name == "accounts" else name.replace("s", "") + "_id" if not name.endswith("es") else name[:-2] + "_id"}')
|
||||
else:
|
||||
path_parts.append(f'/{name}')
|
||||
|
||||
return ''.join(path_parts)
|
||||
|
||||
# Print summary
|
||||
print("=== Chatwoot Controllers & Actions ===")
|
||||
for controller, action in sorted(routes):
|
||||
print(f" {controller}#{action}")
|
||||
print(f"\nTotal controller actions: {len(routes)}")
|
||||
|
||||
# Also group by controller
|
||||
controller_actions = {}
|
||||
for controller, action in routes:
|
||||
if controller not in controller_actions:
|
||||
controller_actions[controller] = []
|
||||
controller_actions[controller].append(action)
|
||||
|
||||
print(f"\nTotal controllers: {len(controller_actions)}")
|
||||
for controller in sorted(controller_actions.keys()):
|
||||
actions = controller_actions[controller]
|
||||
print(f" {controller}: {len(actions)} actions -> {', '.join(actions)}")
|
||||
Reference in New Issue
Block a user