diff --git a/.bandit b/.bandit new file mode 100644 index 0000000..ae25026 --- /dev/null +++ b/.bandit @@ -0,0 +1,3 @@ +[bandit] +exclude_dirs = tests +skips = B101,B110 diff --git a/.dss-session-summary.md b/.dss-session-summary.md index e74eb25..97cafa0 100644 --- a/.dss-session-summary.md +++ b/.dss-session-summary.md @@ -17,4 +17,4 @@ | Untracked | .aidev-config.yaml | --- -*Generated by DSS Session Summary Hook* \ No newline at end of file +*Generated by DSS Session Summary Hook* diff --git a/.dss/components/figma-registry.json b/.dss/components/figma-registry.json index b3ac502..4a757fe 100644 --- a/.dss/components/figma-registry.json +++ b/.dss/components/figma-registry.json @@ -24825,4 +24825,4 @@ "states": {} } } -} \ No newline at end of file +} diff --git a/.dss/data/_system/components.json b/.dss/data/_system/components.json index d454f45..b45ae44 100644 --- a/.dss/data/_system/components.json +++ b/.dss/data/_system/components.json @@ -370,4 +370,4 @@ "properties": {}, "variants": [] } -] \ No newline at end of file +] diff --git a/.dss/data/_system/figma-components.json b/.dss/data/_system/figma-components.json index 5fd9d64..62b7edf 100644 --- a/.dss/data/_system/figma-components.json +++ b/.dss/data/_system/figma-components.json @@ -29298,4 +29298,4 @@ }, "total_components": 3814, "total_sets": 74 -} \ No newline at end of file +} diff --git a/.dss/data/_system/styles.json b/.dss/data/_system/styles.json index d24599a..e979102 100644 --- a/.dss/data/_system/styles.json +++ b/.dss/data/_system/styles.json @@ -405,4 +405,4 @@ ], "GRID": [] } -} \ No newline at end of file +} diff --git a/.dss/data/_system/tokens/resolved-meta.json b/.dss/data/_system/tokens/resolved-meta.json index d22fc25..829b937 100644 --- a/.dss/data/_system/tokens/resolved-meta.json +++ b/.dss/data/_system/tokens/resolved-meta.json @@ -8,4 +8,4 @@ "skins/shadcn", "themes/default" ] -} \ No newline at end of file +} diff --git a/.dss/data/_system/tokens/tokens.json b/.dss/data/_system/tokens/tokens.json index e938f95..8ee7db8 100644 --- a/.dss/data/_system/tokens/tokens.json +++ b/.dss/data/_system/tokens/tokens.json @@ -317,4 +317,4 @@ } } } -} \ No newline at end of file +} diff --git a/.dss/runtime-config.json b/.dss/runtime-config.json index b13182f..10c2ae7 100644 --- a/.dss/runtime-config.json +++ b/.dss/runtime-config.json @@ -25,4 +25,4 @@ "code_gen": true, "ai_advisor": false } -} \ No newline at end of file +} diff --git a/.dss/skins/figma-source/tokens.json b/.dss/skins/figma-source/tokens.json index 48d45f3..cafaab5 100644 --- a/.dss/skins/figma-source/tokens.json +++ b/.dss/skins/figma-source/tokens.json @@ -303,4 +303,4 @@ "_contract": false } } -} \ No newline at end of file +} diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..e354eaa --- /dev/null +++ b/.flake8 @@ -0,0 +1,2 @@ +[flake8] +select = F diff --git a/.gitea/workflows/dss-analysis.yml b/.gitea/workflows/dss-analysis.yml index 23161a6..1a389cb 100644 --- a/.gitea/workflows/dss-analysis.yml +++ b/.gitea/workflows/dss-analysis.yml @@ -6,7 +6,7 @@ on: [push] jobs: dss-context-update: runs-on: ubuntu-latest - + steps: # Step 1: Check out the repository code - name: Checkout code @@ -43,12 +43,12 @@ jobs: # Check if the analysis graph file has been changed if ! git diff --quiet .dss/analysis_graph.json; then echo "Change detected in analysis_graph.json. Committing and pushing..." - + # Add the file, commit, and push back to the same branch. # The GITEA_TOKEN is a secret you must configure in your project settings. git add .dss/analysis_graph.json git commit -m "chore(dss): Update project analysis context [skip ci]" - + # Use the DSS_GITEA_TOKEN for authentication # GITEA_SERVER_URL and GITEA_REPOSITORY are default environment variables in Gitea Actions. git push https://dss-agent:${{ secrets.DSS_GITEA_TOKEN }}@${GITEA_SERVER_URL}/${GITEA_REPOSITORY}.git HEAD:${GITEA_REF_NAME} diff --git a/.githooks/pre-commit-python b/.githooks/pre-commit-python index 5500a77..094f9a5 100755 --- a/.githooks/pre-commit-python +++ b/.githooks/pre-commit-python @@ -11,13 +11,13 @@ Validators: 5. Audit logging """ -import sys -import os import json -import subprocess -from pathlib import Path -from datetime import datetime +import os import re +import subprocess +import sys +from datetime import datetime +from pathlib import Path # Configuration DSS_ROOT = Path("/home/overbits/dss") @@ -32,11 +32,13 @@ IMMUTABLE_FILES = [ AUDIT_LOG = DSS_ROOT / ".dss/logs/git-hooks.jsonl" TEMP_DIR = DSS_ROOT / ".dss/temp" + class Colors: - RED = '\033[0;31m' - GREEN = '\033[0;32m' - YELLOW = '\033[1;33m' - NC = '\033[0m' # No Color + RED = "\033[0;31m" + GREEN = "\033[0;32m" + YELLOW = "\033[1;33m" + NC = "\033[0m" # No Color + def log_audit(validator, status, details): """Log hook events to audit trail""" @@ -53,16 +55,18 @@ def log_audit(validator, status, details): with open(AUDIT_LOG, "a") as f: f.write(json.dumps(log_entry) + "\n") + def get_staged_files(): """Get list of staged files""" result = subprocess.run( ["git", "diff", "--cached", "--name-only", "--diff-filter=ACM"], capture_output=True, text=True, - cwd=DSS_ROOT + cwd=DSS_ROOT, ) return [Path(f) for f in result.stdout.strip().split("\n") if f] + def check_immutable_files(staged_files): """Validate that immutable files are not modified""" from fnmatch import fnmatch @@ -77,7 +81,7 @@ def check_immutable_files(staged_files): ["git", "ls-tree", "--name-only", "HEAD", str(file_path)], capture_output=True, text=True, - cwd=DSS_ROOT + cwd=DSS_ROOT, ) if result.stdout.strip(): # File exists in HEAD violations.append(str(file_path)) @@ -93,27 +97,30 @@ def check_immutable_files(staged_files): commit_msg = commit_msg_file.read_text() if "[IMMUTABLE-UPDATE]" in commit_msg: bypass = True - log_audit("immutable_files", "bypass", { - "files": violations, - "commit_message": commit_msg.split("\n")[0], - "method": "commit_message" - }) + log_audit( + "immutable_files", + "bypass", + { + "files": violations, + "commit_message": commit_msg.split("\n")[0], + "method": "commit_message", + }, + ) if bypass: - log_audit("immutable_files", "bypass", { - "files": violations, - "method": "environment_variable" - }) + log_audit( + "immutable_files", "bypass", {"files": violations, "method": "environment_variable"} + ) if not bypass: print(f"{Colors.RED}✗ IMMUTABLE FILE VIOLATION{Colors.NC}") - print(f"\nThe following protected files cannot be modified:") + print("\nThe following protected files cannot be modified:") for v in violations: print(f" - {v}") - print(f"\nTo update immutable files:") - print(f" 1. Use commit message: [IMMUTABLE-UPDATE] Reason for change") - print(f" 2. Include justification in commit body") - print(f"\nProtected files:") + print("\nTo update immutable files:") + print(" 1. Use commit message: [IMMUTABLE-UPDATE] Reason for change") + print(" 2. Include justification in commit body") + print("\nProtected files:") for pattern in IMMUTABLE_FILES: print(f" - {pattern}") @@ -123,6 +130,7 @@ def check_immutable_files(staged_files): log_audit("immutable_files", "passed", {"files_checked": len(staged_files)}) return True + def check_temp_folder(staged_files): """Validate that temp files are only in .dss/temp/""" violations = [] @@ -151,11 +159,11 @@ def check_temp_folder(staged_files): if violations: print(f"{Colors.RED}✗ TEMP FOLDER VIOLATION{Colors.NC}") - print(f"\nTemp files must be created in .dss/temp/ only:") + print("\nTemp files must be created in .dss/temp/ only:") for v in violations: print(f" - {v}") - print(f"\nAll temporary files MUST go in: .dss/temp/[session-id]/") - print(f"Use the get_temp_dir() helper function.") + print("\nAll temporary files MUST go in: .dss/temp/[session-id]/") + print("Use the get_temp_dir() helper function.") log_audit("temp_folder", "rejected", {"files": violations}) return False @@ -163,6 +171,7 @@ def check_temp_folder(staged_files): log_audit("temp_folder", "passed", {"files_checked": len(staged_files)}) return True + def check_schemas(staged_files): """Validate JSON and YAML schemas""" violations = [] @@ -178,20 +187,18 @@ def check_schemas(staged_files): elif file_path.suffix in [".yaml", ".yml"]: try: import yaml + with open(full_path) as f: yaml.safe_load(f) except ImportError: # YAML not available, skip validation continue except Exception as e: - violations.append({ - "file": str(file_path), - "error": str(e) - }) + violations.append({"file": str(file_path), "error": str(e)}) if violations: print(f"{Colors.RED}✗ SCHEMA VALIDATION FAILED{Colors.NC}") - print(f"\nInvalid JSON/YAML files:") + print("\nInvalid JSON/YAML files:") for v in violations: print(f" - {v['file']}") print(f" Error: {v['error']}") @@ -202,6 +209,7 @@ def check_schemas(staged_files): log_audit("schema_validation", "passed", {"files_checked": len(staged_files)}) return True + def check_documentation(staged_files): """Check that new implementations have documentation""" violations = [] @@ -228,20 +236,24 @@ def check_documentation(staged_files): missing_func_docs = re.findall(func_pattern, content) if missing_class_docs: - warnings.append({ - "file": str(file_path), - "type": "class", - "items": missing_class_docs[:5] # Limit to first 5 - }) + warnings.append( + { + "file": str(file_path), + "type": "class", + "items": missing_class_docs[:5], # Limit to first 5 + } + ) if missing_func_docs: - warnings.append({ - "file": str(file_path), - "type": "function", - "items": missing_func_docs[:5] # Limit to first 5 - }) + warnings.append( + { + "file": str(file_path), + "type": "function", + "items": missing_func_docs[:5], # Limit to first 5 + } + ) - except Exception as e: + except Exception: continue # Check if significant code changes have knowledge updates @@ -251,15 +263,19 @@ def check_documentation(staged_files): # If many code files changed but no knowledge updates, warn if len(code_files_changed) > 5 and len(knowledge_files_changed) == 0: - warnings.append({ - "file": "general", - "type": "knowledge", - "items": [f"Changed {len(code_files_changed)} code files but no .knowledge/ updates"] - }) + warnings.append( + { + "file": "general", + "type": "knowledge", + "items": [ + f"Changed {len(code_files_changed)} code files but no .knowledge/ updates" + ], + } + ) if warnings: print(f"{Colors.YELLOW}⚠ DOCUMENTATION WARNING{Colors.NC}") - print(f"\nMissing documentation found (non-blocking):") + print("\nMissing documentation found (non-blocking):") for w in warnings: if w["type"] == "class": print(f" - {w['file']}: Classes without docstrings: {', '.join(w['items'])}") @@ -267,8 +283,8 @@ def check_documentation(staged_files): print(f" - {w['file']}: Functions without docstrings: {', '.join(w['items'])}") elif w["type"] == "knowledge": print(f" - {w['items'][0]}") - print(f"\n Tip: Add docstrings to new classes/functions") - print(f" Tip: Update .knowledge/ files when adding major features\n") + print("\n Tip: Add docstrings to new classes/functions") + print(" Tip: Update .knowledge/ files when adding major features\n") log_audit("documentation", "warning", {"warnings": warnings}) else: @@ -297,18 +313,16 @@ def check_terminology(staged_files): for old_term, new_term in deprecated_terms.items(): if re.search(rf"\b{old_term}\b", content, re.IGNORECASE): - warnings.append({ - "file": str(file_path), - "term": old_term, - "suggested": new_term - }) + warnings.append( + {"file": str(file_path), "term": old_term, "suggested": new_term} + ) except: # Skip binary or unreadable files continue if warnings: print(f"{Colors.YELLOW}⚠ TERMINOLOGY WARNING{Colors.NC}") - print(f"\nDeprecated terminology found (non-blocking):") + print("\nDeprecated terminology found (non-blocking):") for w in warnings: print(f" - {w['file']}: '{w['term']}' → use '{w['suggested']}'") print() @@ -320,6 +334,7 @@ def check_terminology(staged_files): # Always return True (warnings only) return True + def main(): """Run all validators""" print(f"{Colors.GREEN}Running DSS pre-commit validations...{Colors.NC}\n") @@ -356,9 +371,10 @@ def main(): return 0 else: print(f"\n{Colors.RED}✗ Pre-commit validation failed{Colors.NC}") - print(f"Fix the issues above and try again.\n") + print("Fix the issues above and try again.\n") log_audit("pre_commit", "failed", {"files": len(staged_files)}) return 1 + if __name__ == "__main__": sys.exit(main()) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7822579..0626ea8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -41,23 +41,13 @@ repos: rev: 7.0.0 hooks: - id: flake8 - args: [--max-line-length=100, --extend-ignore=E203] additional_dependencies: [flake8-docstrings] - # Python type checking (optional, can be slow) - # - repo: https://github.com/pre-commit/mirrors-mypy - # rev: v1.8.0 - # hooks: - # - id: mypy - # additional_dependencies: [types-all] - # args: [--ignore-missing-imports, --no-strict-optional] - # Security checks - repo: https://github.com/PyCQA/bandit rev: 1.7.6 hooks: - id: bandit - args: [-r, tools/, -ll] exclude: tests/ # Configuration diff --git a/admin-ui/.dss/analysis_graph.json b/admin-ui/.dss/analysis_graph.json index 3eef4bb..5a6739f 100644 --- a/admin-ui/.dss/analysis_graph.json +++ b/admin-ui/.dss/analysis_graph.json @@ -848,4 +848,4 @@ } ], "links": [] -} \ No newline at end of file +} diff --git a/admin-ui/.storybook/main.js b/admin-ui/.storybook/main.js index 0d1fd67..c74b512 100644 --- a/admin-ui/.storybook/main.js +++ b/admin-ui/.storybook/main.js @@ -22,4 +22,4 @@ const config = { return config; } }; -export default config; \ No newline at end of file +export default config; diff --git a/admin-ui/.storybook/preview.js b/admin-ui/.storybook/preview.js index c69a74b..4f16270 100644 --- a/admin-ui/.storybook/preview.js +++ b/admin-ui/.storybook/preview.js @@ -20,4 +20,4 @@ const preview = { }, }; -export default preview; \ No newline at end of file +export default preview; diff --git a/admin-ui/css/tokens.css b/admin-ui/css/tokens.css index 67906bd..45007cd 100644 --- a/admin-ui/css/tokens.css +++ b/admin-ui/css/tokens.css @@ -75,7 +75,7 @@ --font-semibold: 600; --font-medium: 500; --font-bold: 700; - + --text-xs: 0.75rem; /* 12px */ --text-sm: 0.875rem; /* 14px */ --text-base: 1rem; /* 16px */ diff --git a/admin-ui/ds.config.json b/admin-ui/ds.config.json index b6407b4..d85b082 100644 --- a/admin-ui/ds.config.json +++ b/admin-ui/ds.config.json @@ -25,4 +25,4 @@ }, "created_at": "2025-12-10T12:52:18.513773", "updated_at": "2025-12-10T13:46:05.807775" -} \ No newline at end of file +} diff --git a/admin-ui/public/admin-ui/css/tokens.css b/admin-ui/public/admin-ui/css/tokens.css index 67906bd..45007cd 100644 --- a/admin-ui/public/admin-ui/css/tokens.css +++ b/admin-ui/public/admin-ui/css/tokens.css @@ -75,7 +75,7 @@ --font-semibold: 600; --font-medium: 500; --font-bold: 700; - + --text-xs: 0.75rem; /* 12px */ --text-sm: 0.875rem; /* 14px */ --text-base: 1rem; /* 16px */ diff --git a/admin-ui/src/stories/ColorPrimitives.stories.js b/admin-ui/src/stories/ColorPrimitives.stories.js index 1303896..14cee57 100644 --- a/admin-ui/src/stories/ColorPrimitives.stories.js +++ b/admin-ui/src/stories/ColorPrimitives.stories.js @@ -35,7 +35,7 @@ export const AllColors = { render: () => `
- +

Base

@@ -55,7 +55,7 @@ export const AllColors = {
transparent
- +

Neutral Scales

@@ -245,7 +245,7 @@ export const AllColors = {
- +

Semantic Scales

diff --git a/admin-ui/src/stories/ComponentsDataDisplay.stories.js b/admin-ui/src/stories/ComponentsDataDisplay.stories.js index 8e0ef02..6ab6f31 100644 --- a/admin-ui/src/stories/ComponentsDataDisplay.stories.js +++ b/admin-ui/src/stories/ComponentsDataDisplay.stories.js @@ -59,7 +59,7 @@ export const Overview = { render: () => `
- +

Accordion

@@ -67,7 +67,7 @@ export const Overview = {

A vertically stacked set of interactive headings that reveal content sections

- +
@@ -76,21 +76,21 @@ export const Overview = {

An image element with a fallback for user profile images

size: sm md lg
- +

Badge

- +

Displays a badge or label

variant: default secondary destructive outline
- +

Carousel

- +

A carousel with embla-carousel

@@ -99,7 +99,7 @@ export const Overview = {

Chart

- +

Beautiful charts using Recharts

@@ -112,12 +112,12 @@ export const Overview = {

An interactive component that expands/collapses content

- +

Data Table

- +

Powerful table with sorting, filtering, pagination

@@ -126,38 +126,38 @@ export const Overview = {

Item

- +

Generic list item component

- +

Kbd

- +

Keyboard key display

- +

Table

- +

Styled HTML table

- +

Typography

- +

Text styling components

- +
` diff --git a/admin-ui/src/stories/ComponentsFeedback.stories.js b/admin-ui/src/stories/ComponentsFeedback.stories.js index 99d4232..72d9f91 100644 --- a/admin-ui/src/stories/ComponentsFeedback.stories.js +++ b/admin-ui/src/stories/ComponentsFeedback.stories.js @@ -59,24 +59,24 @@ export const Overview = { render: () => `
- +

Alert

- +

Displays a callout for user attention

variant: default destructive
- +

Empty

- +

Empty state display

- +
@@ -85,21 +85,21 @@ export const Overview = {

Progress indicator bar

- +

Skeleton

- +

Loading placeholder

- +

Sonner

- +

Toast notifications with sonner

@@ -108,11 +108,11 @@ export const Overview = {

Spinner

- +

Loading spinner animation

- +
@@ -121,7 +121,7 @@ export const Overview = {

Toast notification component

variant: default destructive
- +
` diff --git a/admin-ui/src/stories/ComponentsFormComponents.stories.js b/admin-ui/src/stories/ComponentsFormComponents.stories.js index 98033c1..b4c33e5 100644 --- a/admin-ui/src/stories/ComponentsFormComponents.stories.js +++ b/admin-ui/src/stories/ComponentsFormComponents.stories.js @@ -59,29 +59,29 @@ export const Overview = { render: () => `
- +

Button

- +

Displays a button or a component that looks like a button

variant: default destructive outline secondary
size: default sm lg icon
- +

Button Group

- +

Groups multiple buttons together

- +

Calendar

- +

A date picker component with monthly/yearly views

@@ -94,21 +94,21 @@ export const Overview = {

A control that allows toggling between checked and unchecked

- +

Combobox

- +

Autocomplete input with command palette

- +

Command

- +

Command palette with search and filtering

@@ -117,25 +117,25 @@ export const Overview = {

Date Picker

- +

A date picker built with calendar and popover

- +

Field

- +

Form field wrapper with label and error

- +

Form

- +

Form component with react-hook-form integration

@@ -144,25 +144,25 @@ export const Overview = {

Input

- +

Text input field

- +

Input Group

- +

Group of inputs with addons

- +

Input OTP

- +

One-time password input

@@ -175,16 +175,16 @@ export const Overview = {

Text label for form elements

- +

Native Select

- +

Native HTML select element with styling

- +
@@ -193,7 +193,7 @@ export const Overview = {

Set of mutually exclusive options

- +
@@ -202,7 +202,7 @@ export const Overview = {

Custom select dropdown

- +
@@ -211,7 +211,7 @@ export const Overview = {

Range slider input

- +
@@ -220,16 +220,16 @@ export const Overview = {

Toggle switch control

- +

Textarea

- +

Multi-line text input

- +
@@ -238,7 +238,7 @@ export const Overview = {

Two-state button

variant: default outline
size: default sm lg
- +
@@ -247,7 +247,7 @@ export const Overview = {

Group of toggle buttons

- +
` diff --git a/admin-ui/src/stories/ComponentsLayout.stories.js b/admin-ui/src/stories/ComponentsLayout.stories.js index 7a8852d..eb16ca1 100644 --- a/admin-ui/src/stories/ComponentsLayout.stories.js +++ b/admin-ui/src/stories/ComponentsLayout.stories.js @@ -59,7 +59,7 @@ export const Overview = { render: () => `
- +

Aspect Ratio

@@ -67,21 +67,21 @@ export const Overview = {

Displays content with a desired aspect ratio

- +

Card

- +

Displays a card with header, content, and footer

- +

Resizable

- +

Resizable panel groups

@@ -94,7 +94,7 @@ export const Overview = {

Custom scrollbar styling

- +
@@ -103,7 +103,7 @@ export const Overview = {

Visual divider

orientation: horizontal vertical
- +
` diff --git a/admin-ui/src/stories/ComponentsNavigation.stories.js b/admin-ui/src/stories/ComponentsNavigation.stories.js index 97d3ddf..fb56e82 100644 --- a/admin-ui/src/stories/ComponentsNavigation.stories.js +++ b/admin-ui/src/stories/ComponentsNavigation.stories.js @@ -59,15 +59,15 @@ export const Overview = { render: () => `
- +

Breadcrumb

- +

Displays the path to the current page in a hierarchy

- +
@@ -76,7 +76,7 @@ export const Overview = {

Horizontal menu with dropdowns

- +
@@ -85,25 +85,25 @@ export const Overview = {

Website navigation with mega menus

- +

Pagination

- +

Page navigation with previous/next

- +

Sidebar

- +

Application sidebar with collapsible sections

- +
@@ -112,7 +112,7 @@ export const Overview = {

Tabbed interface

- +
` diff --git a/admin-ui/src/stories/ComponentsOverlay.stories.js b/admin-ui/src/stories/ComponentsOverlay.stories.js index 2e2b92e..c9a18ae 100644 --- a/admin-ui/src/stories/ComponentsOverlay.stories.js +++ b/admin-ui/src/stories/ComponentsOverlay.stories.js @@ -59,7 +59,7 @@ export const Overview = { render: () => `
- +

Alert Dialog

@@ -67,7 +67,7 @@ export const Overview = {

A modal dialog that interrupts user flow with important information

- +
@@ -76,7 +76,7 @@ export const Overview = {

Right-click context menu with keyboard support

- +
@@ -85,12 +85,12 @@ export const Overview = {

A modal dialog for content display

- +

Drawer

- +

A drawer component extending dialog

@@ -103,7 +103,7 @@ export const Overview = {

Menu displayed on trigger interaction

- +
@@ -112,7 +112,7 @@ export const Overview = {

Content appearing on hover

- +
@@ -121,7 +121,7 @@ export const Overview = {

Floating content panel

- +
@@ -130,7 +130,7 @@ export const Overview = {

Side panel overlay

side: top right bottom left
- +
@@ -139,7 +139,7 @@ export const Overview = {

Informative popup on hover

- +
` diff --git a/admin-ui/src/stories/Effects.stories.js b/admin-ui/src/stories/Effects.stories.js index d467bfe..07f1cef 100644 --- a/admin-ui/src/stories/Effects.stories.js +++ b/admin-ui/src/stories/Effects.stories.js @@ -33,7 +33,7 @@ export const Shadows = { render: () => `
- +
shadow-xs
diff --git a/admin-ui/src/stories/Radius.stories.js b/admin-ui/src/stories/Radius.stories.js index f7d6a9f..16dfb79 100644 --- a/admin-ui/src/stories/Radius.stories.js +++ b/admin-ui/src/stories/Radius.stories.js @@ -27,7 +27,7 @@ export const RadiusScale = { render: () => `
- +
none
diff --git a/admin-ui/src/stories/SemanticColors.stories.js b/admin-ui/src/stories/SemanticColors.stories.js index 8b8c1bb..927b31f 100644 --- a/admin-ui/src/stories/SemanticColors.stories.js +++ b/admin-ui/src/stories/SemanticColors.stories.js @@ -30,7 +30,7 @@ export const LightTheme = { render: () => `
- +

Surface

diff --git a/admin-ui/src/stories/Shadows.stories.js b/admin-ui/src/stories/Shadows.stories.js index 0e4a8e1..1083fad 100644 --- a/admin-ui/src/stories/Shadows.stories.js +++ b/admin-ui/src/stories/Shadows.stories.js @@ -27,7 +27,7 @@ export const AllShadows = { render: () => `
- +
none
diff --git a/admin-ui/src/stories/Spacing.stories.js b/admin-ui/src/stories/Spacing.stories.js index d05eb9c..d628822 100644 --- a/admin-ui/src/stories/Spacing.stories.js +++ b/admin-ui/src/stories/Spacing.stories.js @@ -28,7 +28,7 @@ export const SpacingScale = { render: () => `
- +
diff --git a/admin-ui/src/stories/Typography.stories.js b/admin-ui/src/stories/Typography.stories.js index a28e1d2..99a76b2 100644 --- a/admin-ui/src/stories/Typography.stories.js +++ b/admin-ui/src/stories/Typography.stories.js @@ -41,7 +41,7 @@ export const FontFamilies = {

Font Families

- +
The quick brown fox jumps over the lazy dog @@ -214,7 +214,7 @@ export const TextStyles = {

Composed Text Styles

- +
The quick brown fox diff --git a/admin-ui/theme.json b/admin-ui/theme.json index 3ee700f..fb1a148 100644 --- a/admin-ui/theme.json +++ b/admin-ui/theme.json @@ -5,7 +5,7 @@ "author": "DSS Team", "license": "MIT", "homepage": "https://github.com/anthropics/dss", - + "metadata": { "type": "design-system", "architecture": "layered-css", diff --git a/apps/api/ai_providers.py b/apps/api/ai_providers.py index 7ae497b..cf62185 100644 --- a/apps/api/ai_providers.py +++ b/apps/api/ai_providers.py @@ -1,17 +1,18 @@ """ -AI Provider abstraction for Claude and Gemini +AI Provider abstraction for Claude and Gemini. + Handles model-specific API calls and tool execution """ -import os -import json import asyncio -from typing import List, Dict, Any, Optional +import json +import os from abc import ABC, abstractmethod +from typing import Any, Dict, List, Optional class AIProvider(ABC): - """Abstract base class for AI providers""" + """Abstract base class for AI providers.""" @abstractmethod async def chat( @@ -20,7 +21,7 @@ class AIProvider(ABC): system_prompt: str, history: List[Dict[str, Any]], tools: Optional[List[Dict[str, Any]]] = None, - temperature: float = 0.7 + temperature: float = 0.7, ) -> Dict[str, Any]: """ Send a chat message and get response @@ -36,16 +37,17 @@ class AIProvider(ABC): class ClaudeProvider(AIProvider): - """Anthropic Claude provider""" + """Anthropic Claude provider.""" def __init__(self): self.api_key = os.getenv("ANTHROPIC_API_KEY") self.default_model = "claude-sonnet-4-5-20250929" def is_available(self) -> bool: - """Check if Claude is available""" + """Check if Claude is available.""" try: from anthropic import Anthropic + return bool(self.api_key) except ImportError: return False @@ -58,9 +60,9 @@ class ClaudeProvider(AIProvider): tools: Optional[List[Dict[str, Any]]] = None, temperature: float = 0.7, mcp_handler=None, - mcp_context=None + mcp_context=None, ) -> Dict[str, Any]: - """Chat with Claude""" + """Chat with Claude.""" if not self.is_available(): return { @@ -68,7 +70,7 @@ class ClaudeProvider(AIProvider): "response": "Claude not available. Install anthropic SDK or set ANTHROPIC_API_KEY.", "model": "error", "tools_used": [], - "stop_reason": "error" + "stop_reason": "error", } from anthropic import Anthropic @@ -91,17 +93,14 @@ class ClaudeProvider(AIProvider): "max_tokens": 4096, "temperature": temperature, "system": system_prompt, - "messages": messages + "messages": messages, } if tools: api_params["tools"] = tools # Initial call - response = await asyncio.to_thread( - client.messages.create, - **api_params - ) + response = await asyncio.to_thread(client.messages.create, **api_params) # Handle tool use loop tools_used = [] @@ -120,16 +119,16 @@ class ClaudeProvider(AIProvider): # Execute tool via MCP handler result = await mcp_handler.execute_tool( - tool_name=tool_name, - arguments=tool_input, - context=mcp_context + tool_name=tool_name, arguments=tool_input, context=mcp_context ) - tools_used.append({ - "tool": tool_name, - "success": result.success, - "duration_ms": result.duration_ms - }) + tools_used.append( + { + "tool": tool_name, + "success": result.success, + "duration_ms": result.duration_ms, + } + ) # Format result if result.success: @@ -137,19 +136,20 @@ class ClaudeProvider(AIProvider): else: tool_result_content = json.dumps({"error": result.error}) - tool_results.append({ - "type": "tool_result", - "tool_use_id": tool_use_id, - "content": tool_result_content - }) + tool_results.append( + { + "type": "tool_result", + "tool_use_id": tool_use_id, + "content": tool_result_content, + } + ) # Continue conversation with tool results messages.append({"role": "assistant", "content": response.content}) messages.append({"role": "user", "content": tool_results}) response = await asyncio.to_thread( - client.messages.create, - **{**api_params, "messages": messages} + client.messages.create, **{**api_params, "messages": messages} ) # Extract final response @@ -163,27 +163,30 @@ class ClaudeProvider(AIProvider): "response": response_text, "model": response.model, "tools_used": tools_used, - "stop_reason": response.stop_reason + "stop_reason": response.stop_reason, } class GeminiProvider(AIProvider): - """Google Gemini provider""" + """Google Gemini provider.""" def __init__(self): self.api_key = os.getenv("GOOGLE_API_KEY") or os.getenv("GEMINI_API_KEY") self.default_model = "gemini-2.0-flash-exp" def is_available(self) -> bool: - """Check if Gemini is available""" + """Check if Gemini is available.""" try: import google.generativeai as genai + return bool(self.api_key) except ImportError: return False - def _convert_tools_to_gemini_format(self, claude_tools: List[Dict[str, Any]]) -> List[Dict[str, Any]]: - """Convert Claude tool format to Gemini function declarations""" + def _convert_tools_to_gemini_format( + self, claude_tools: List[Dict[str, Any]] + ) -> List[Dict[str, Any]]: + """Convert Claude tool format to Gemini function declarations.""" gemini_tools = [] for tool in claude_tools: @@ -191,11 +194,7 @@ class GeminiProvider(AIProvider): function_declaration = { "name": tool.get("name"), "description": tool.get("description", ""), - "parameters": { - "type": "object", - "properties": {}, - "required": [] - } + "parameters": {"type": "object", "properties": {}, "required": []}, } # Convert input schema @@ -218,9 +217,9 @@ class GeminiProvider(AIProvider): tools: Optional[List[Dict[str, Any]]] = None, temperature: float = 0.7, mcp_handler=None, - mcp_context=None + mcp_context=None, ) -> Dict[str, Any]: - """Chat with Gemini""" + """Chat with Gemini.""" if not self.is_available(): return { @@ -228,7 +227,7 @@ class GeminiProvider(AIProvider): "response": "Gemini not available. Install google-generativeai SDK or set GOOGLE_API_KEY/GEMINI_API_KEY.", "model": "error", "tools_used": [], - "stop_reason": "error" + "stop_reason": "error", } import google.generativeai as genai @@ -241,10 +240,9 @@ class GeminiProvider(AIProvider): role = msg.get("role", "user") content = msg.get("content", "") if content and role in ["user", "assistant"]: - gemini_history.append({ - "role": "user" if role == "user" else "model", - "parts": [content] - }) + gemini_history.append( + {"role": "user" if role == "user" else "model", "parts": [content]} + ) # Create model with tools if available model_kwargs = { @@ -253,7 +251,7 @@ class GeminiProvider(AIProvider): "temperature": temperature, "max_output_tokens": 4096, }, - "system_instruction": system_prompt + "system_instruction": system_prompt, } # Convert and add tools if available @@ -282,7 +280,7 @@ class GeminiProvider(AIProvider): has_function_call = False for part in response.candidates[0].content.parts: - if hasattr(part, 'function_call') and part.function_call: + if hasattr(part, "function_call") and part.function_call: has_function_call = True func_call = part.function_call tool_name = func_call.name @@ -290,31 +288,34 @@ class GeminiProvider(AIProvider): # Execute tool result = await mcp_handler.execute_tool( - tool_name=tool_name, - arguments=tool_args, - context=mcp_context + tool_name=tool_name, arguments=tool_args, context=mcp_context ) - tools_used.append({ - "tool": tool_name, - "success": result.success, - "duration_ms": result.duration_ms - }) + tools_used.append( + { + "tool": tool_name, + "success": result.success, + "duration_ms": result.duration_ms, + } + ) # Format result for Gemini function_response = { "name": tool_name, - "response": result.result if result.success else {"error": result.error} + "response": result.result + if result.success + else {"error": result.error}, } # Send function response back current_message = genai.protos.Content( - parts=[genai.protos.Part( - function_response=genai.protos.FunctionResponse( - name=tool_name, - response=function_response + parts=[ + genai.protos.Part( + function_response=genai.protos.FunctionResponse( + name=tool_name, response=function_response + ) ) - )] + ] ) break @@ -328,7 +329,7 @@ class GeminiProvider(AIProvider): response_text = "" if response.candidates and response.candidates[0].content.parts: for part in response.candidates[0].content.parts: - if hasattr(part, 'text'): + if hasattr(part, "text"): response_text += part.text return { @@ -336,13 +337,13 @@ class GeminiProvider(AIProvider): "response": response_text, "model": self.default_model, "tools_used": tools_used, - "stop_reason": "stop" if response.candidates else "error" + "stop_reason": "stop" if response.candidates else "error", } # Factory function def get_ai_provider(model_name: str) -> AIProvider: - """Get AI provider by name""" + """Get AI provider by name.""" if model_name.lower() in ["gemini", "google"]: return GeminiProvider() else: diff --git a/apps/api/browser_logger.py b/apps/api/browser_logger.py index 7330bd2..10cd216 100644 --- a/apps/api/browser_logger.py +++ b/apps/api/browser_logger.py @@ -1,9 +1,10 @@ -import os import logging +import os from logging.handlers import RotatingFileHandler +from typing import Any, List, Optional + from fastapi import APIRouter, HTTPException from pydantic import BaseModel -from typing import List, Any, Optional # --- Configuration --- # Use project-local logs directory to avoid permission issues @@ -21,30 +22,29 @@ browser_logger = logging.getLogger("browser_logger") browser_logger.setLevel(logging.INFO) # Rotating file handler: 10MB max size, keep last 5 backups -handler = RotatingFileHandler(LOG_FILE, maxBytes=10*1024*1024, backupCount=5) -formatter = logging.Formatter( - '%(asctime)s [%(levelname)s] [BROWSER] %(message)s' -) +handler = RotatingFileHandler(LOG_FILE, maxBytes=10 * 1024 * 1024, backupCount=5) +formatter = logging.Formatter("%(asctime)s [%(levelname)s] [BROWSER] %(message)s") handler.setFormatter(formatter) browser_logger.addHandler(handler) # --- API Router --- router = APIRouter() + class LogEntry(BaseModel): level: str timestamp: str message: str data: Optional[List[Any]] = None + class LogBatch(BaseModel): logs: List[LogEntry] + @router.post("/api/logs/browser") async def receive_browser_logs(batch: LogBatch): - """ - Receives a batch of logs from the browser and writes them to the log file. - """ + """Receives a batch of logs from the browser and writes them to the log file.""" try: for log in batch.logs: # Map browser levels to python logging levels @@ -52,11 +52,11 @@ async def receive_browser_logs(batch: LogBatch): log_message = f"[{log.timestamp}] {log.message}" - if level == 'error': + if level == "error": browser_logger.error(log_message) - elif level == 'warn': + elif level == "warn": browser_logger.warning(log_message) - elif level == 'debug': + elif level == "debug": browser_logger.debug(log_message) else: browser_logger.info(log_message) diff --git a/apps/api/server.py b/apps/api/server.py index 33f8ce6..6d03925 100644 --- a/apps/api/server.py +++ b/apps/api/server.py @@ -1,5 +1,5 @@ """ -DSS API Server +DSS API Server. REST API for design system operations. @@ -16,10 +16,44 @@ Modes: - Local: Development companion """ -# Load environment variables from .env file FIRST (before any other imports) +import json import os +import subprocess +import sys +from datetime import datetime from pathlib import Path +from typing import Any, Dict, List, Optional + from dotenv import load_dotenv +from fastapi import BackgroundTasks, Depends, FastAPI, Header, HTTPException, Query +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import JSONResponse +from fastapi.staticfiles import StaticFiles +from pydantic import BaseModel + +from apps.api.browser_logger import router as browser_log_router +from dss import settings + +# Load environment variables from .env file FIRST (before any other imports) +from dss.auth.atlassian_auth import get_auth +from dss.figma.figma_tools import FigmaToolSuite +from dss.services.config_service import ConfigService +from dss.services.project_manager import ProjectManager +from dss.services.sandboxed_fs import SandboxedFS +from dss.storage.json_store import ( + ActivityLog, + Cache, + CodeMetrics, + Components, + FigmaFiles, + IntegrationHealth, + Integrations, + Projects, + SyncHistory, + Teams, + TestResults, + get_stats, +) # Get project root - apps/api/server.py -> apps/api -> apps -> project_root _server_file = Path(__file__).resolve() @@ -27,46 +61,22 @@ _project_root = _server_file.parent.parent.parent # /home/.../dss # Try loading from multiple possible .env locations env_paths = [ - _project_root / ".env", # root .env (primary) + _project_root / ".env", # root .env (primary) _project_root / "storybook" / ".env", # storybook/.env - _server_file.parent / ".env", # apps/api/.env + _server_file.parent / ".env", # apps/api/.env ] for env_path in env_paths: if env_path.exists(): load_dotenv(env_path, override=True) break -import asyncio -import subprocess -import json -from typing import Optional, List, Dict, Any -from datetime import datetime -from fastapi import FastAPI, HTTPException, Query, BackgroundTasks, Depends, Header -from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import JSONResponse -from fastapi.staticfiles import StaticFiles -from pydantic import BaseModel -from typing import Optional - -import sys # Add project root to path for dss package sys.path.insert(0, str(Path(__file__).parent.parent.parent)) # Import browser logger router (local import from same directory) -from apps.api.browser_logger import router as browser_log_router # DSS package imports - unified package -from dss import settings -from dss.storage.json_store import ( - Projects, Components, SyncHistory, ActivityLog, Teams, Cache, get_stats, - FigmaFiles, CodeMetrics, TestResults, TokenDrift, Tokens, Styles, - Integrations, IntegrationHealth -) -from dss.figma.figma_tools import FigmaToolSuite -from dss.services.project_manager import ProjectManager -from dss.services.config_service import ConfigService, DSSConfig -from dss.services.sandboxed_fs import SandboxedFS # Additional DSS imports available: # from dss import DesignToken, TokenSource, ProjectScanner @@ -81,52 +91,69 @@ class _FigmaConfigCompat: @property def is_configured(self): return settings.figma_configured + @property def token(self): return settings.FIGMA_TOKEN + @property def cache_ttl(self): return settings.FIGMA_CACHE_TTL + class _ServerConfigCompat: @property def env(self): return settings.SERVER_ENV + @property def port(self): return settings.SERVER_PORT + @property def host(self): return settings.SERVER_HOST + @property def is_production(self): return settings.is_production + class _ConfigCompat: figma = _FigmaConfigCompat() server = _ServerConfigCompat() def summary(self): return { - "figma": {"configured": settings.figma_configured, "cache_ttl": settings.FIGMA_CACHE_TTL}, - "server": {"port": settings.SERVER_PORT, "env": settings.SERVER_ENV, "log_level": settings.LOG_LEVEL}, + "figma": { + "configured": settings.figma_configured, + "cache_ttl": settings.FIGMA_CACHE_TTL, + }, + "server": { + "port": settings.SERVER_PORT, + "env": settings.SERVER_ENV, + "log_level": settings.LOG_LEVEL, + }, "database": {"path": str(settings.DATABASE_PATH)}, } + config = _ConfigCompat() # === Runtime Configuration === + class RuntimeConfig: """ - ⚙️ ENDOCRINE HORMONE STORAGE - Runtime configuration system + ⚙️ ENDOCRINE HORMONE STORAGE - Runtime configuration system. The endocrine system regulates behavior through hormones. This configuration manager stores the component's behavioral preferences and adaptation state. Persists to .dss/runtime-config.json so the component remembers its preferences even after sleep (shutdown). """ + def __init__(self): self.config_path = Path(__file__).parent.parent.parent / ".dss" / "runtime-config.json" self.config_path.parent.mkdir(parents=True, exist_ok=True) @@ -136,7 +163,7 @@ class RuntimeConfig: if self.config_path.exists(): try: return json.loads(self.config_path.read_text()) - except (json.JSONDecodeError, IOError) as e: + except (json.JSONDecodeError, IOError): # Config file corrupted or unreadable, use defaults pass return { @@ -152,7 +179,7 @@ class RuntimeConfig: "token_sync": True, "code_gen": True, "ai_advisor": False, - } + }, } def _save(self): @@ -205,9 +232,11 @@ ProjectManager.ensure_schema() # === Service Discovery === + class ServiceDiscovery: """ Service discovery for companion services (Storybook, Chromatic, dev servers). + Checks known ports to discover running services. """ @@ -230,13 +259,13 @@ class ServiceDiscovery: try: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(0.5) - result = sock.connect_ex(('127.0.0.1', port)) + result = sock.connect_ex(("127.0.0.1", port)) sock.close() if result == 0: discovered[service] = { "running": True, "port": port, - "url": f"http://localhost:{port}" + "url": f"http://localhost:{port}", } break except (OSError, socket.error): @@ -259,11 +288,7 @@ class ServiceDiscovery: try: async with httpx.AsyncClient(timeout=2.0) as client: resp = await client.get(url) - return { - "running": resp.status_code == 200, - "url": url, - "port": port - } + return {"running": resp.status_code == 200, "url": url, "port": port} except (httpx.ConnectError, httpx.TimeoutException, httpx.HTTPError): # Storybook not running or unreachable return {"running": False, "url": url, "port": port} @@ -274,7 +299,7 @@ class ServiceDiscovery: app = FastAPI( title="Design System Server (DSS)", description="API for design system management and Figma integration", - version="1.0.0" + version="1.0.0", ) app.add_middleware( @@ -298,18 +323,20 @@ figma_config = runtime_config.get("figma") figma_token_at_startup = figma_config.get("token") if figma_config else None figma_suite = FigmaToolSuite( token=figma_token_at_startup, - output_dir=str(Path(__file__).parent.parent.parent / ".dss" / "output") + output_dir=str(Path(__file__).parent.parent.parent / ".dss" / "output"), ) # === Request/Response Models === + class ProjectCreate(BaseModel): name: str description: str = "" figma_file_key: str = "" root_path: str = "" # MVP1: Project root directory path + class ProjectUpdate(BaseModel): name: Optional[str] = None description: Optional[str] = None @@ -317,30 +344,36 @@ class ProjectUpdate(BaseModel): status: Optional[str] = None root_path: Optional[str] = None # MVP1: Update project root path + class FigmaExtractRequest(BaseModel): file_key: str format: str = "css" + class FigmaSyncRequest(BaseModel): file_key: str target_path: str format: str = "css" + class TeamCreate(BaseModel): name: str description: str = "" + class FigmaFileCreate(BaseModel): figma_url: str file_name: str file_key: str + class ESRECreate(BaseModel): name: str definition_text: str expected_value: Optional[str] = None component_name: Optional[str] = None + class TokenDriftCreate(BaseModel): component_id: str property_name: str @@ -353,11 +386,11 @@ class TokenDriftCreate(BaseModel): # === Authentication === -from dss.auth.atlassian_auth import get_auth async def get_current_user(authorization: Optional[str] = Header(None)) -> Dict[str, Any]: """ Dependency to get current authenticated user from JWT token. + Usage: user = Depends(get_current_user) """ if not authorization or not authorization.startswith("Bearer "): @@ -372,12 +405,14 @@ async def get_current_user(authorization: Optional[str] = Header(None)) -> Dict[ return user_data + class LoginRequest(BaseModel): url: str # Atlassian URL email: str api_token: str service: str = "jira" # "jira" or "confluence" + @app.post("/api/auth/login") async def login(request: LoginRequest): """ @@ -392,7 +427,7 @@ async def login(request: LoginRequest): url=request.url, email=request.email, api_token=request.api_token, - service=request.service + service=request.service, ) return result except ValueError as e: @@ -400,21 +435,25 @@ async def login(request: LoginRequest): except Exception as e: raise HTTPException(status_code=500, detail=f"Login failed: {str(e)}") + @app.get("/api/auth/me") async def get_me(user: Dict[str, Any] = Depends(get_current_user)): - """Get current authenticated user info""" + """Get current authenticated user info.""" auth = get_auth() user_data = await auth.get_user_by_id(user["user_id"]) if not user_data: raise HTTPException(status_code=404, detail="User not found") return user_data + # === Root & Health === + @app.get("/") async def root(): """Redirect to Admin UI dashboard.""" from fastapi.responses import RedirectResponse + return RedirectResponse(url="/admin-ui/index.html") @@ -432,16 +471,17 @@ async def health(): - Figma - Is the Figma integration configured? """ import os - import psutil from pathlib import Path + import psutil + # Check storage connectivity storage_ok = False try: from dss.storage.json_store import DATA_DIR + storage_ok = DATA_DIR.exists() except Exception as e: - import traceback print(f"[Health] Storage check error: {type(e).__name__}: {e}", flush=True) # Check MCP handler functionality @@ -449,21 +489,24 @@ async def health(): try: import sys from pathlib import Path + project_root = Path(__file__).parent.parent.parent if str(project_root) not in sys.path: sys.path.insert(0, str(project_root)) from dss.mcp.handler import get_mcp_handler + handler = get_mcp_handler() mcp_ok = handler is not None except Exception as e: - import traceback print(f"[Health] MCP handler check error: {type(e).__name__}: {e}", flush=True) # Get uptime try: process = psutil.Process(os.getpid()) - uptime_seconds = int((datetime.now() - datetime.fromtimestamp(process.create_time())).total_seconds()) + uptime_seconds = int( + (datetime.now() - datetime.fromtimestamp(process.create_time())).total_seconds() + ) except: uptime_seconds = 0 @@ -478,17 +521,18 @@ async def health(): "services": { "storage": "ok" if storage_ok else "error", "mcp": "ok" if mcp_ok else "error", - "figma": "connected" if config.figma.is_configured else "not configured" - } + "figma": "connected" if config.figma.is_configured else "not configured", + }, } # === DEBUG ENDPOINTS === + @app.post("/api/browser-logs") async def receive_browser_logs(logs: dict): """ - 📋 BROWSER LOG COLLECTION ENDPOINT + 📋 BROWSER LOG COLLECTION ENDPOINT. Receives browser logs from the dashboard and stores them for debugging. Browser logger (browser-logger.js) POSTs logs here automatically or on demand. @@ -501,8 +545,8 @@ async def receive_browser_logs(logs: dict): "diagnostic": {...} } """ - from pathlib import Path import time + from pathlib import Path # Create browser logs directory if doesn't exist browser_logs_dir = Path(__file__).parent.parent.parent / ".dss" / "browser-logs" @@ -518,13 +562,19 @@ async def receive_browser_logs(logs: dict): # Log to activity (skip if ActivityLog not available) try: with get_connection() as conn: - conn.execute(""" + conn.execute( + """ INSERT INTO activity_log (category, action, details, metadata, created_at) VALUES (?, ?, ?, ?, ?) - """, ("debug", "browser_logs_received", - f"Received browser logs for session {session_id}", - json.dumps({"session_id": session_id, "log_count": len(logs.get("logs", []))}), - datetime.utcnow().isoformat())) + """, + ( + "debug", + "browser_logs_received", + f"Received browser logs for session {session_id}", + json.dumps({"session_id": session_id, "log_count": len(logs.get("logs", []))}), + datetime.utcnow().isoformat(), + ), + ) conn.commit() except: pass # Activity logging is optional @@ -537,25 +587,25 @@ async def receive_browser_logs(logs: dict): # Create task for Claude to investigate try: import httpx + task_data = { "title": f"Browser errors detected in session {session_id[:20]}...", "description": f"Detected {error_count} errors and {warn_count} warnings in browser session. Use dss_get_browser_errors('{session_id}') to investigate.", "priority": 3 if error_count > 0 else 5, "project": "dss-debug", - "visibility": "public" + "visibility": "public", } # Create task via task-queue MCP HTTP endpoint (if available) # This runs async - don't block browser log storage import asyncio + async def create_task(): try: async with httpx.AsyncClient() as client: # Task queue typically runs on same server await client.post( - "http://localhost:8765/tasks", - json=task_data, - timeout=2.0 + "http://localhost:8765/tasks", json=task_data, timeout=2.0 ) except: pass # Task creation is best-effort @@ -570,14 +620,14 @@ async def receive_browser_logs(logs: dict): "sessionId": session_id, "logCount": len(logs.get("logs", [])), "storedAt": datetime.utcnow().isoformat() + "Z", - "errorsDetected": error_count > 0 or warn_count > 0 + "errorsDetected": error_count > 0 or warn_count > 0, } @app.get("/api/browser-logs/{session_id}") async def get_browser_logs(session_id: str): """ - 📋 RETRIEVE BROWSER LOGS + 📋 RETRIEVE BROWSER LOGS. Retrieves stored browser logs by session ID. """ @@ -596,7 +646,7 @@ async def get_browser_logs(session_id: str): @app.get("/api/debug/diagnostic") async def get_debug_diagnostic(): """ - 🔍 COMPREHENSIVE SYSTEM DIAGNOSTIC + 🔍 COMPREHENSIVE SYSTEM DIAGNOSTIC. Returns detailed system diagnostic including: - Health status (from /health endpoint) @@ -607,9 +657,10 @@ async def get_debug_diagnostic(): - Recent errors """ import os - import psutil from pathlib import Path + import psutil + # Get health status health_status = await health() @@ -629,20 +680,17 @@ async def get_debug_diagnostic(): # Get recent errors from activity log try: with get_connection() as conn: - recent_errors = conn.execute(""" + recent_errors = conn.execute( + """ SELECT category, action, details, created_at FROM activity_log WHERE category = 'error' OR action LIKE '%error%' OR action LIKE '%fail%' ORDER BY created_at DESC LIMIT 10 - """).fetchall() + """ + ).fetchall() recent_errors = [ - { - "category": row[0], - "action": row[1], - "details": row[2], - "timestamp": row[3] - } + {"category": row[0], "action": row[1], "details": row[2], "timestamp": row[3]} for row in recent_errors ] except: @@ -652,29 +700,26 @@ async def get_debug_diagnostic(): "status": health_status["status"], "timestamp": datetime.utcnow().isoformat() + "Z", "health": health_status, - "browser": { - "session_count": browser_sessions, - "logs_directory": str(browser_logs_dir) - }, + "browser": {"session_count": browser_sessions, "logs_directory": str(browser_logs_dir)}, "database": { "size_bytes": db_size_bytes, "size_mb": round(db_size_bytes / 1024 / 1024, 2), - "path": str(db_path) + "path": str(db_path), }, "process": { "pid": os.getpid(), "memory_rss_mb": round(memory_info.rss / 1024 / 1024, 2), "memory_vms_mb": round(memory_info.vms / 1024 / 1024, 2), - "threads": process.num_threads() + "threads": process.num_threads(), }, - "recent_errors": recent_errors + "recent_errors": recent_errors, } @app.get("/api/debug/workflows") async def list_workflows(): """ - 📋 LIST AVAILABLE DEBUG WORKFLOWS + 📋 LIST AVAILABLE DEBUG WORKFLOWS. Returns list of available workflows from .dss/WORKFLOWS/ directory. Each workflow is a markdown file with step-by-step debugging procedures. @@ -709,19 +754,17 @@ async def list_workflows(): purpose = line.replace("**Purpose**:", "").strip() break - workflows.append({ - "id": workflow_file.stem, - "title": title, - "purpose": purpose, - "file": workflow_file.name, - "path": str(workflow_file) - }) + workflows.append( + { + "id": workflow_file.stem, + "title": title, + "purpose": purpose, + "file": workflow_file.name, + "path": str(workflow_file), + } + ) - return { - "workflows": workflows, - "count": len(workflows), - "directory": str(workflows_dir) - } + return {"workflows": workflows, "count": len(workflows), "directory": str(workflows_dir)} @app.get("/api/config") @@ -738,6 +781,7 @@ async def get_config(): # Import here to avoid circular imports try: from config import get_public_config + return get_public_config() except ImportError: # Fallback for legacy deployments @@ -747,27 +791,27 @@ async def get_config(): "storybookPort": 6006, } + @app.get("/api/stats") async def get_statistics(): """Get database and system statistics.""" db_stats = get_stats() return { "database": db_stats, - "figma": { - "mode": figma_suite.mode, - "configured": config.figma.is_configured - } + "figma": {"mode": figma_suite.mode, "configured": config.figma.is_configured}, } # === Projects === + @app.get("/api/projects") async def list_projects(status: Optional[str] = None): """List all projects.""" projects = Projects.list(status=status) return projects + @app.get("/api/projects/{project_id}") async def get_project(project_id: str): """Get a specific project.""" @@ -776,6 +820,7 @@ async def get_project(project_id: str): raise HTTPException(status_code=404, detail="Project not found") return project + @app.post("/api/projects") async def create_project(project: ProjectCreate): """Create a new project.""" @@ -784,17 +829,18 @@ async def create_project(project: ProjectCreate): id=project_id, name=project.name, description=project.description, - figma_file_key=project.figma_file_key + figma_file_key=project.figma_file_key, ) ActivityLog.log( action="project_created", entity_type="project", entity_id=project_id, project_id=project_id, - details={"name": project.name} + details={"name": project.name}, ) return created + @app.put("/api/projects/{project_id}") async def update_project(project_id: str, update: ProjectUpdate): """Update a project.""" @@ -812,25 +858,23 @@ async def update_project(project_id: str, update: ProjectUpdate): entity_type="project", entity_id=project_id, project_id=project_id, - details=update_data + details=update_data, ) return updated + @app.delete("/api/projects/{project_id}") async def delete_project(project_id: str): """Delete a project.""" if not Projects.delete(project_id): raise HTTPException(status_code=404, detail="Project not found") - ActivityLog.log( - action="project_deleted", - entity_type="project", - entity_id=project_id - ) + ActivityLog.log(action="project_deleted", entity_type="project", entity_id=project_id) return {"success": True} # === Components === + @app.get("/api/projects/{project_id}/components") async def list_components(project_id: str): """List components for a project.""" @@ -841,6 +885,7 @@ async def list_components(project_id: str): # === Figma Integration === + @app.post("/api/figma/extract-variables") async def extract_variables(request: FigmaExtractRequest, background_tasks: BackgroundTasks): """Extract design tokens from Figma variables.""" @@ -849,12 +894,17 @@ async def extract_variables(request: FigmaExtractRequest, background_tasks: Back ActivityLog.log( action="figma_extract_variables", entity_type="figma", - details={"file_key": request.file_key, "format": request.format, "tokens_count": result.get("tokens_count")} + details={ + "file_key": request.file_key, + "format": request.format, + "tokens_count": result.get("tokens_count"), + }, ) return result except Exception as e: raise HTTPException(status_code=500, detail=f"Token extraction failed: {str(e)}") + @app.post("/api/figma/extract-components") async def extract_components(request: FigmaExtractRequest): """Extract component definitions from Figma.""" @@ -863,12 +913,13 @@ async def extract_components(request: FigmaExtractRequest): ActivityLog.log( action="figma_extract_components", entity_type="figma", - details={"file_key": request.file_key, "count": result.get("components_count")} + details={"file_key": request.file_key, "count": result.get("components_count")}, ) return result except Exception as e: raise HTTPException(status_code=500, detail=str(e)) + @app.post("/api/figma/extract-styles") async def extract_styles(request: FigmaExtractRequest): """Extract style definitions from Figma.""" @@ -878,20 +929,28 @@ async def extract_styles(request: FigmaExtractRequest): except Exception as e: raise HTTPException(status_code=500, detail=f"Style extraction failed: {str(e)}") + @app.post("/api/figma/sync-tokens") async def sync_tokens(request: FigmaSyncRequest): """Sync tokens from Figma to target file.""" try: - result = await figma_suite.sync_tokens(request.file_key, request.target_path, request.format) + result = await figma_suite.sync_tokens( + request.file_key, request.target_path, request.format + ) ActivityLog.log( action="figma_sync_tokens", entity_type="figma", - details={"file_key": request.file_key, "target": request.target_path, "tokens_synced": result.get("tokens_synced")} + details={ + "file_key": request.file_key, + "target": request.target_path, + "tokens_synced": result.get("tokens_synced"), + }, ) return result except Exception as e: raise HTTPException(status_code=500, detail=f"Token sync failed: {str(e)}") + @app.post("/api/figma/validate") async def validate_components(request: FigmaExtractRequest): """Validate component definitions against design system rules.""" @@ -901,6 +960,7 @@ async def validate_components(request: FigmaExtractRequest): except Exception as e: raise HTTPException(status_code=500, detail=str(e)) + @app.post("/api/figma/generate-code") async def generate_code(file_key: str, component_name: str, framework: str = "webcomponent"): """Generate component code from Figma.""" @@ -910,19 +970,23 @@ async def generate_code(file_key: str, component_name: str, framework: str = "we except Exception as e: raise HTTPException(status_code=500, detail=str(e)) + @app.get("/api/figma/health") async def figma_health(): """Check Figma connection status.""" - is_live = figma_suite.mode == 'live' + is_live = figma_suite.mode == "live" return { "status": "ok" if is_live else "degraded", "mode": figma_suite.mode, - "message": "Figma connected" if is_live else "Running in mock mode. Configure FIGMA_TOKEN for live API." + "message": "Figma connected" + if is_live + else "Running in mock mode. Configure FIGMA_TOKEN for live API.", } # === Discovery === + @app.get("/api/discovery") async def run_discovery(path: str = "."): """Run project discovery.""" @@ -930,10 +994,7 @@ async def run_discovery(path: str = "."): try: result = subprocess.run( - [str(script_path), path], - capture_output=True, - text=True, - timeout=30 + [str(script_path), path], capture_output=True, text=True, timeout=30 ) if result.returncode == 0: return json.loads(result.stdout) @@ -944,10 +1005,12 @@ async def run_discovery(path: str = "."): except json.JSONDecodeError: return {"raw_output": result.stdout} + class DiscoveryScanRequest(BaseModel): path: str = "." full_scan: bool = False + @app.post("/api/discovery/scan") async def scan_project(request: DiscoveryScanRequest): """Run project discovery scan.""" @@ -955,17 +1018,14 @@ async def scan_project(request: DiscoveryScanRequest): try: result = subprocess.run( - [str(script_path), request.path], - capture_output=True, - text=True, - timeout=30 + [str(script_path), request.path], capture_output=True, text=True, timeout=30 ) if result.returncode == 0: data = json.loads(result.stdout) ActivityLog.log( action="discovery_scan", entity_type="project", - details={"path": request.path, "full_scan": request.full_scan} + details={"path": request.path, "full_scan": request.full_scan}, ) return data else: @@ -975,6 +1035,7 @@ async def scan_project(request: DiscoveryScanRequest): except json.JSONDecodeError: return {"raw_output": result.stdout} + @app.get("/api/discovery/stats") async def get_discovery_stats(): """Get project statistics.""" @@ -987,34 +1048,30 @@ async def get_discovery_stats(): "today": 0, "this_week": 0, "total": db_stats.get("syncs", {}).get("total", 0), - "last_sync": None + "last_sync": None, }, - "stories": { - "total": 0 - } + "stories": {"total": 0}, } + @app.get("/api/discovery/activity") async def get_discovery_activity(limit: int = Query(default=10, le=50)): """Get recent discovery activity.""" return ActivityLog.recent(limit=limit) + @app.get("/api/discovery/ports") async def discover_ports(): """Discover listening ports and services.""" script_path = Path(__file__).parent.parent / "discovery" / "discover-ports.sh" try: - result = subprocess.run( - [str(script_path)], - capture_output=True, - text=True, - timeout=10 - ) + result = subprocess.run([str(script_path)], capture_output=True, text=True, timeout=10) return json.loads(result.stdout) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) + @app.get("/api/discovery/env") async def discover_env(path: str = "."): """Analyze environment configuration.""" @@ -1022,10 +1079,7 @@ async def discover_env(path: str = "."): try: result = subprocess.run( - [str(script_path), path], - capture_output=True, - text=True, - timeout=10 + [str(script_path), path], capture_output=True, text=True, timeout=10 ) return json.loads(result.stdout) except Exception as e: @@ -1034,19 +1088,24 @@ async def discover_env(path: str = "."): # === Activity & Sync History === + @app.get("/api/activity") async def get_activity(limit: int = Query(default=50, le=100)): """Get recent activity log.""" return ActivityLog.recent(limit=limit) + @app.get("/api/sync-history") -async def get_sync_history(project_id: Optional[str] = None, limit: int = Query(default=20, le=100)): +async def get_sync_history( + project_id: Optional[str] = None, limit: int = Query(default=20, le=100) +): """Get sync history.""" return SyncHistory.recent(project_id=project_id, limit=limit) # === Audit Log (Enhanced) === + @app.get("/api/audit") async def get_audit_log( project_id: Optional[str] = None, @@ -1058,7 +1117,7 @@ async def get_audit_log( start_date: Optional[str] = None, end_date: Optional[str] = None, limit: int = Query(default=50, le=200), - offset: int = Query(default=0, ge=0) + offset: int = Query(default=0, ge=0), ): """ Get audit log with advanced filtering. @@ -1085,14 +1144,11 @@ async def get_audit_log( start_date=start_date, end_date=end_date, limit=limit, - offset=offset + offset=offset, ) total = ActivityLog.count( - project_id=project_id, - user_id=user_id, - action=action, - category=category + project_id=project_id, user_id=user_id, action=action, category=category ) return { @@ -1100,28 +1156,32 @@ async def get_audit_log( "total": total, "limit": limit, "offset": offset, - "has_more": (offset + limit) < total + "has_more": (offset + limit) < total, } + @app.get("/api/audit/stats") async def get_audit_stats(): """Get audit log statistics.""" return { "by_category": ActivityLog.get_stats_by_category(), "by_user": ActivityLog.get_stats_by_user(), - "total_count": ActivityLog.count() + "total_count": ActivityLog.count(), } + @app.get("/api/audit/categories") async def get_audit_categories(): """Get list of all activity categories.""" return ActivityLog.get_categories() + @app.get("/api/audit/actions") async def get_audit_actions(): """Get list of all activity actions.""" return ActivityLog.get_actions() + class AuditLogRequest(BaseModel): action: str entity_type: Optional[str] = None @@ -1133,18 +1193,20 @@ class AuditLogRequest(BaseModel): team_context: Optional[str] = None description: Optional[str] = None category: Optional[str] = None - severity: str = 'info' + severity: str = "info" details: Optional[Dict[str, Any]] = None + @app.post("/api/audit") async def create_audit_entry(entry: AuditLogRequest, request: Any): """ Create a new audit log entry. + Automatically captures IP and user agent from request. """ # Extract IP and user agent from request - ip_address = request.client.host if hasattr(request, 'client') else None - user_agent = request.headers.get('user-agent') if hasattr(request, 'headers') else None + ip_address = request.client.host if hasattr(request, "client") else None + user_agent = request.headers.get("user-agent") if hasattr(request, "headers") else None ActivityLog.log( action=entry.action, @@ -1160,39 +1222,49 @@ async def create_audit_entry(entry: AuditLogRequest, request: Any): severity=entry.severity, details=entry.details, ip_address=ip_address, - user_agent=user_agent + user_agent=user_agent, ) return {"success": True, "message": "Audit entry created"} + @app.get("/api/audit/export") async def export_audit_log( project_id: Optional[str] = None, category: Optional[str] = None, start_date: Optional[str] = None, end_date: Optional[str] = None, - format: str = Query(default="json", regex="^(json|csv)$") + format: str = Query(default="json", regex="^(json|csv)$"), ): - """ - Export audit log in JSON or CSV format. - """ + """Export audit log in JSON or CSV format.""" activities = ActivityLog.search( project_id=project_id, category=category, start_date=start_date, end_date=end_date, - limit=10000 # Max export limit + limit=10000, # Max export limit ) if format == "csv": import csv import io + from fastapi.responses import StreamingResponse output = io.StringIO() if activities: - fieldnames = ['created_at', 'user_name', 'action', 'category', 'description', 'project_id', 'entity_type', 'entity_name', 'severity'] - writer = csv.DictWriter(output, fieldnames=fieldnames, extrasaction='ignore') + fieldnames = [ + "created_at", + "user_name", + "action", + "category", + "description", + "project_id", + "entity_type", + "entity_name", + "severity", + ] + writer = csv.DictWriter(output, fieldnames=fieldnames, extrasaction="ignore") writer.writeheader() writer.writerows(activities) @@ -1200,24 +1272,28 @@ async def export_audit_log( return StreamingResponse( iter([output.getvalue()]), media_type="text/csv", - headers={"Content-Disposition": f"attachment; filename=audit_log_{datetime.utcnow().strftime('%Y%m%d_%H%M%S')}.csv"} + headers={ + "Content-Disposition": f"attachment; filename=audit_log_{datetime.utcnow().strftime('%Y%m%d_%H%M%S')}.csv" + }, ) else: # JSON format return { "activities": activities, "total": len(activities), - "exported_at": datetime.utcnow().isoformat() + "Z" + "exported_at": datetime.utcnow().isoformat() + "Z", } # === Teams === + @app.get("/api/teams") async def list_teams(): """List all teams.""" return Teams.list() + @app.post("/api/teams") async def create_team(team: TeamCreate): """Create a new team.""" @@ -1225,6 +1301,7 @@ async def create_team(team: TeamCreate): created = Teams.create(team_id, team.name, team.description) return created + @app.get("/api/teams/{team_id}") async def get_team(team_id: str): """Get a specific team.""" @@ -1236,12 +1313,14 @@ async def get_team(team_id: str): # === Cache Management === + @app.post("/api/cache/clear") async def clear_cache(): """Clear expired cache entries.""" count = Cache.clear_expired() return {"cleared": count} + @app.delete("/api/cache") async def purge_cache(): """Purge all cache entries.""" @@ -1251,6 +1330,7 @@ async def purge_cache(): # === Configuration Management === + class ConfigUpdate(BaseModel): mode: Optional[str] = None figma_token: Optional[str] = None @@ -1264,7 +1344,7 @@ async def get_config(): return { "config": runtime_config.get(), "env": config.summary(), - "mode": runtime_config.get("mode") + "mode": runtime_config.get("mode"), } @@ -1282,12 +1362,12 @@ async def update_config(update: ConfigUpdate): global figma_suite figma_suite = FigmaToolSuite( token=update.figma_token, - output_dir=str(Path(__file__).parent.parent.parent / ".dss" / "output") + output_dir=str(Path(__file__).parent.parent.parent / ".dss" / "output"), ) ActivityLog.log( action="figma_token_updated", entity_type="config", - details={"configured": bool(update.figma_token)} + details={"configured": bool(update.figma_token)}, ) if update.services: @@ -1299,9 +1379,7 @@ async def update_config(update: ConfigUpdate): if updates: runtime_config.update(updates) ActivityLog.log( - action="config_updated", - entity_type="config", - details={"keys": list(updates.keys())} + action="config_updated", entity_type="config", details={"keys": list(updates.keys())} ) return runtime_config.get() @@ -1321,7 +1399,7 @@ async def get_figma_config(): "sync_tokens": True, "validate": True, "generate_code": True, - } + }, } @@ -1335,18 +1413,16 @@ async def test_figma_connection(): # Test with a minimal API call import httpx + token = runtime_config._data["figma"]["token"] async with httpx.AsyncClient() as client: - resp = await client.get( - "https://api.figma.com/v1/me", - headers={"X-Figma-Token": token} - ) + resp = await client.get("https://api.figma.com/v1/me", headers={"X-Figma-Token": token}) if resp.status_code == 200: user = resp.json() return { "success": True, "user": user.get("email", "connected"), - "handle": user.get("handle") + "handle": user.get("handle"), } else: return {"success": False, "error": f"API returned {resp.status_code}"} @@ -1356,6 +1432,7 @@ async def test_figma_connection(): # === Service Discovery === + @app.get("/api/services") async def list_services(): """List configured and discovered services.""" @@ -1365,7 +1442,7 @@ async def list_services(): return { "configured": configured, "discovered": discovered, - "storybook": await ServiceDiscovery.check_storybook() + "storybook": await ServiceDiscovery.check_storybook(), } @@ -1380,7 +1457,7 @@ async def configure_service(service_name: str, config_data: Dict[str, Any]): action="service_configured", entity_type="service", entity_id=service_name, - details={"keys": list(config_data.keys())} + details={"keys": list(config_data.keys())}, ) return services[service_name] @@ -1451,7 +1528,7 @@ async def init_storybook(request_data: Dict[str, Any] = None): results = await generator.generate_stories_for_directory( str(source_path.relative_to(dss_mvp1_path)), template=StoryTemplate.CSF3, - dry_run=False + dry_run=False, ) # Move generated stories to stories/generated/ @@ -1478,26 +1555,23 @@ async def init_storybook(request_data: Dict[str, Any] = None): ActivityLog.log( action="storybook_initialized", entity_type="storybook", - details={ - "stories_generated": stories_generated, - "errors_count": len(errors) - } + details={"stories_generated": stories_generated, "errors_count": len(errors)}, ) return { "success": True, "stories_generated": stories_generated, - "message": f"Generated {stories_generated} stories" if stories_generated > 0 else "Storybook initialized (no components found)", - "errors": errors if errors else None + "message": f"Generated {stories_generated} stories" + if stories_generated > 0 + else "Storybook initialized (no components found)", + "errors": errors if errors else None, } except HTTPException: raise except Exception as e: ActivityLog.log( - action="storybook_init_failed", - entity_type="storybook", - details={"error": str(e)} + action="storybook_init_failed", entity_type="storybook", details={"error": str(e)} ) raise HTTPException(status_code=500, detail=f"Storybook initialization failed: {str(e)}") @@ -1506,6 +1580,7 @@ async def init_storybook(request_data: Dict[str, Any] = None): async def clear_storybook_stories(): """ Clear all generated stories from Storybook. + Returns Storybook to blank state (only Welcome page). """ import shutil @@ -1527,13 +1602,13 @@ async def clear_storybook_stories(): ActivityLog.log( action="storybook_cleared", entity_type="storybook", - details={"cleared_count": cleared_count} + details={"cleared_count": cleared_count}, ) return { "success": True, "cleared_count": cleared_count, - "message": "Storybook stories cleared" + "message": "Storybook stories cleared", } except Exception as e: @@ -1542,13 +1617,17 @@ async def clear_storybook_stories(): # === Design System Ingestion === + class IngestionRequest(BaseModel): """Request for design system ingestion via natural language.""" + prompt: str project_id: Optional[str] = None + class IngestionConfirmRequest(BaseModel): """Confirm ingestion of a specific design system.""" + system_id: str method: str = "npm" # npm, figma, css, manual source_url: Optional[str] = None @@ -1580,8 +1659,8 @@ async def parse_ingestion_prompt(request: IngestionRequest): details={ "prompt": request.prompt[:100], "intent": result.get("intent"), - "sources_found": len(result.get("sources", [])) - } + "sources_found": len(result.get("sources", [])), + }, ) return result @@ -1592,9 +1671,7 @@ async def parse_ingestion_prompt(request: IngestionRequest): @app.get("/api/ingest/systems") async def list_known_systems( - category: Optional[str] = None, - framework: Optional[str] = None, - search: Optional[str] = None + category: Optional[str] = None, framework: Optional[str] = None, search: Optional[str] = None ): """ List known design systems from the registry. @@ -1609,7 +1686,7 @@ async def list_known_systems( get_all_systems, get_systems_by_category, get_systems_by_framework, - search_design_systems + search_design_systems, ) if search: @@ -1624,11 +1701,7 @@ async def list_known_systems( return { "systems": [s.to_dict() for s in systems], "count": len(systems), - "filters": { - "category": category, - "framework": framework, - "search": search - } + "filters": {"category": category, "framework": framework, "search": search}, } except Exception as e: @@ -1637,9 +1710,7 @@ async def list_known_systems( @app.get("/api/ingest/systems/{system_id}") async def get_system_info(system_id: str): - """ - Get detailed information about a specific design system. - """ + """Get detailed information about a specific design system.""" try: from design_system_registry import find_design_system, get_alternative_ingestion_options @@ -1650,10 +1721,7 @@ async def get_system_info(system_id: str): alternatives = get_alternative_ingestion_options(system) - return { - "system": system.to_dict(), - "alternatives": alternatives - } + return {"system": system.to_dict(), "alternatives": alternatives} except HTTPException: raise @@ -1663,9 +1731,7 @@ async def get_system_info(system_id: str): @app.get("/api/ingest/npm/search") async def search_npm_packages( - query: str, - limit: int = Query(default=10, le=50), - design_systems_only: bool = True + query: str, limit: int = Query(default=10, le=50), design_systems_only: bool = True ): """ Search npm registry for design system packages. @@ -1681,7 +1747,7 @@ async def search_npm_packages( "packages": [r.to_dict() for r in results], "count": len(results), "query": query, - "design_systems_only": design_systems_only + "design_systems_only": design_systems_only, } except Exception as e: @@ -1733,12 +1799,12 @@ async def confirm_ingestion(request: IngestionConfirmRequest): if not system: # Try to find via npm from npm_search import get_package_info + npm_info = await get_package_info(request.system_id) if not npm_info: raise HTTPException( - status_code=404, - detail=f"Design system not found: {request.system_id}" + status_code=404, detail=f"Design system not found: {request.system_id}" ) # Execute ingestion based on method @@ -1746,7 +1812,7 @@ async def confirm_ingestion(request: IngestionConfirmRequest): "success": True, "system_id": request.system_id, "method": request.method, - "status": "queued" + "status": "queued", } if request.method == "npm": @@ -1758,22 +1824,19 @@ async def confirm_ingestion(request: IngestionConfirmRequest): "Install npm packages", "Extract design tokens", "Generate Storybook stories", - "Update token configuration" + "Update token configuration", ] elif request.method == "figma": if not request.source_url: - raise HTTPException( - status_code=400, - detail="Figma URL required for figma method" - ) + raise HTTPException(status_code=400, detail="Figma URL required for figma method") result["figma_url"] = request.source_url result["message"] = "Will extract tokens from Figma" result["next_steps"] = [ "Authenticate with Figma", "Extract design tokens", "Map to CSS variables", - "Generate component stories" + "Generate component stories", ] elif request.method == "css": @@ -1782,17 +1845,14 @@ async def confirm_ingestion(request: IngestionConfirmRequest): if system and system.css_cdn_url: request.source_url = system.css_cdn_url else: - raise HTTPException( - status_code=400, - detail="CSS URL required for css method" - ) + raise HTTPException(status_code=400, detail="CSS URL required for css method") result["css_url"] = request.source_url result["message"] = "Will parse CSS for design tokens" result["next_steps"] = [ "Fetch CSS file", "Parse CSS variables", "Extract color/spacing/typography tokens", - "Create token collection" + "Create token collection", ] elif request.method == "manual": @@ -1801,17 +1861,14 @@ async def confirm_ingestion(request: IngestionConfirmRequest): "Enter color tokens", "Enter typography tokens", "Enter spacing tokens", - "Review and confirm" + "Review and confirm", ] ActivityLog.log( action="ingestion_confirmed", entity_type="ingestion", entity_id=request.system_id, - details={ - "method": request.method, - "status": "queued" - } + details={"method": request.method, "status": "queued"}, ) return result @@ -1827,7 +1884,7 @@ async def execute_ingestion( system_id: str, method: str = "npm", source_url: Optional[str] = None, - project_id: Optional[str] = None + project_id: Optional[str] = None, ): """ Execute the actual ingestion process. @@ -1858,10 +1915,12 @@ async def execute_ingestion( if system.css_cdn_url: # Fetch CSS from CDN and parse import httpx + async with httpx.AsyncClient() as client: resp = await client.get(system.css_cdn_url) if resp.status_code == 200: from dss.ingest.css import CSSTokenSource + # Write temp file and parse temp_css = Path("/tmp") / f"{system.id}_tokens.css" temp_css.write_text(resp.text) @@ -1874,7 +1933,7 @@ async def execute_ingestion( # For Tailwind-based systems, we'll need their config tokens_extracted = 0 # Placeholder for Tailwind parsing - except ImportError as e: + except ImportError: # Token ingestion module not available pass finally: @@ -1889,6 +1948,7 @@ async def execute_ingestion( elif method == "css" and source_url: # Fetch and parse CSS import httpx + sys.path.insert(0, str(Path(__file__).parent.parent.parent / "dss-mvp1")) try: @@ -1896,6 +1956,7 @@ async def execute_ingestion( resp = await client.get(source_url) if resp.status_code == 200: from dss.ingest.css import CSSTokenSource + temp_css = Path("/tmp") / "ingested_tokens.css" temp_css.write_text(resp.text) source = CSSTokenSource(str(temp_css)) @@ -1910,10 +1971,7 @@ async def execute_ingestion( entity_type="ingestion", entity_id=system_id, project_id=project_id, - details={ - "method": method, - "tokens_extracted": tokens_extracted - } + details={"method": method, "tokens_extracted": tokens_extracted}, ) return { @@ -1921,7 +1979,7 @@ async def execute_ingestion( "system_id": system_id, "method": method, "tokens_extracted": tokens_extracted, - "message": f"Extracted {tokens_extracted} tokens from {system.name if system else system_id}" + "message": f"Extracted {tokens_extracted} tokens from {system.name if system else system_id}", } except Exception as e: @@ -1929,7 +1987,7 @@ async def execute_ingestion( action="ingestion_failed", entity_type="ingestion", entity_id=system_id, - details={"error": str(e)} + details={"error": str(e)}, ) raise HTTPException(status_code=500, detail=str(e)) @@ -1957,6 +2015,7 @@ async def get_ingestion_alternatives(system_id: Optional[str] = None): # === DSS Mode === + @app.get("/api/mode") async def get_mode(): """Get current DSS mode.""" @@ -1964,7 +2023,7 @@ async def get_mode(): return { "mode": mode, "description": "Local dev companion" if mode == "local" else "Remote design system server", - "features": runtime_config.get("features") + "features": runtime_config.get("features"), } @@ -1976,11 +2035,7 @@ async def set_mode(request_data: Dict[str, Any]): raise HTTPException(status_code=400, detail="Mode must be 'local' or 'server'") runtime_config.set("mode", mode) - ActivityLog.log( - action="mode_changed", - entity_type="config", - details={"mode": mode} - ) + ActivityLog.log(action="mode_changed", entity_type="config", details={"mode": mode}) return {"mode": mode, "success": True} @@ -1993,10 +2048,12 @@ async def set_mode(request_data: Dict[str, Any]): # === System Administration === + @app.post("/api/system/reset") async def reset_dss(request_data: Dict[str, Any]): """ Reset DSS to fresh state by calling the reset command in dss-mvp1. + Requires confirmation. """ confirm = request_data.get("confirm", "") @@ -2014,41 +2071,35 @@ async def reset_dss(request_data: Dict[str, Any]): cwd=str(dss_mvp1_path), capture_output=True, text=True, - timeout=60 + timeout=60, ) if result.returncode != 0: raise Exception(f"Reset failed: {result.stderr}") - ActivityLog.log( - action="dss_reset", - entity_type="system", - details={"status": "success"} - ) + ActivityLog.log(action="dss_reset", entity_type="system", details={"status": "success"}) return { "success": True, "message": "DSS has been reset to fresh state", - "output": result.stdout + "output": result.stdout, } except subprocess.TimeoutExpired: raise HTTPException(status_code=504, detail="Reset operation timed out") except Exception as e: - ActivityLog.log( - action="dss_reset_failed", - entity_type="system", - details={"error": str(e)} - ) + ActivityLog.log(action="dss_reset_failed", entity_type="system", details={"error": str(e)}) raise HTTPException(status_code=500, detail=str(e)) # === Team Dashboards === + @app.get("/api/projects/{project_id}/dashboard/summary") async def get_dashboard_summary(project_id: str): """ Get dashboard summary for all teams (thin slice). + Provides overview of UX, UI, and QA metrics. """ if not Projects.get(project_id): @@ -2069,21 +2120,16 @@ async def get_dashboard_summary(project_id: str): "project_id": project_id, "ux": { "figma_files_count": len(figma_files), - "figma_files": figma_files[:5] # Show first 5 + "figma_files": figma_files[:5], # Show first 5 }, - "ui": { - "token_drift": drift_stats, - "code_metrics": code_summary - }, - "qa": { - "esre_count": len(esre_list), - "test_summary": test_summary - } + "ui": {"token_drift": drift_stats, "code_metrics": code_summary}, + "qa": {"esre_count": len(esre_list), "test_summary": test_summary}, } # === UX Dashboard: Figma File Management === + @app.get("/api/projects/{project_id}/figma-files") async def list_figma_files(project_id: str): """List all Figma files for a project (UX Dashboard).""" @@ -2102,17 +2148,17 @@ async def create_figma_file(project_id: str, figma_file: FigmaFileCreate): project_id=project_id, figma_url=figma_file.figma_url, file_name=figma_file.file_name, - file_key=figma_file.file_key + file_key=figma_file.file_key, ) ActivityLog.log( action="figma_file_added", entity_type="figma_file", - entity_id=str(created['id']), + entity_id=str(created["id"]), entity_name=figma_file.file_name, project_id=project_id, team_context="ux", - details={"file_key": figma_file.file_key} + details={"file_key": figma_file.file_key}, ) return created @@ -2125,9 +2171,7 @@ async def update_figma_file_sync(project_id: str, file_id: int, status: str = "s raise HTTPException(status_code=404, detail="Project not found") updated = FigmaFiles.update_sync_status( - file_id=file_id, - status=status, - last_synced=datetime.utcnow().isoformat() + file_id=file_id, status=status, last_synced=datetime.utcnow().isoformat() ) if not updated: @@ -2138,7 +2182,7 @@ async def update_figma_file_sync(project_id: str, file_id: int, status: str = "s entity_type="figma_file", entity_id=str(file_id), project_id=project_id, - team_context="ux" + team_context="ux", ) return updated @@ -2158,7 +2202,7 @@ async def delete_figma_file(project_id: str, file_id: int): entity_type="figma_file", entity_id=str(file_id), project_id=project_id, - team_context="ux" + team_context="ux", ) return {"success": True} @@ -2166,6 +2210,7 @@ async def delete_figma_file(project_id: str, file_id: int): # === UI Dashboard: Token Drift Detection === + @app.get("/api/projects/{project_id}/token-drift") async def list_token_drift(project_id: str, severity: Optional[str] = None): """List token drift issues for a project (UI Dashboard).""" @@ -2175,10 +2220,7 @@ async def list_token_drift(project_id: str, severity: Optional[str] = None): drifts = TokenDriftDetector.list_by_project(project_id, severity) stats = TokenDriftDetector.get_stats(project_id) - return { - "drifts": drifts, - "stats": stats - } + return {"drifts": drifts, "stats": stats} @app.post("/api/projects/{project_id}/token-drift") @@ -2194,19 +2236,16 @@ async def record_token_drift(project_id: str, drift: TokenDriftCreate): file_path=drift.file_path, line_number=drift.line_number, severity=drift.severity, - suggested_token=drift.suggested_token + suggested_token=drift.suggested_token, ) ActivityLog.log( action="token_drift_detected", entity_type="token_drift", - entity_id=str(created['id']), + entity_id=str(created["id"]), project_id=project_id, team_context="ui", - details={ - "severity": drift.severity, - "component_id": drift.component_id - } + details={"severity": drift.severity, "component_id": drift.component_id}, ) return created @@ -2232,7 +2271,7 @@ async def update_drift_status(project_id: str, drift_id: int, status: str): entity_id=str(drift_id), project_id=project_id, team_context="ui", - details={"status": status} + details={"status": status}, ) return updated @@ -2240,6 +2279,7 @@ async def update_drift_status(project_id: str, drift_id: int, status: str): # === QA Dashboard: ESRE Definitions === + @app.get("/api/projects/{project_id}/esre") async def list_esre_definitions(project_id: str): """List all ESRE definitions for a project (QA Dashboard).""" @@ -2259,16 +2299,16 @@ async def create_esre_definition(project_id: str, esre: ESRECreate): name=esre.name, definition_text=esre.definition_text, expected_value=esre.expected_value, - component_name=esre.component_name + component_name=esre.component_name, ) ActivityLog.log( action="esre_created", entity_type="esre", - entity_id=str(created['id']), + entity_id=str(created["id"]), entity_name=esre.name, project_id=project_id, - team_context="qa" + team_context="qa", ) return created @@ -2285,7 +2325,7 @@ async def update_esre_definition(project_id: str, esre_id: int, updates: ESRECre name=updates.name, definition_text=updates.definition_text, expected_value=updates.expected_value, - component_name=updates.component_name + component_name=updates.component_name, ) if not updated: @@ -2297,7 +2337,7 @@ async def update_esre_definition(project_id: str, esre_id: int, updates: ESRECre entity_id=str(esre_id), entity_name=updates.name, project_id=project_id, - team_context="qa" + team_context="qa", ) return updated @@ -2317,7 +2357,7 @@ async def delete_esre_definition(project_id: str, esre_id: int): entity_type="esre", entity_id=str(esre_id), project_id=project_id, - team_context="qa" + team_context="qa", ) return {"success": True} @@ -2325,8 +2365,10 @@ async def delete_esre_definition(project_id: str, esre_id: int): # === Claude Chat API with MCP Tool Integration === + class ClaudeChatRequest(BaseModel): - """AI chat request model (supports Claude and Gemini)""" + """AI chat request model (supports Claude and Gemini).""" + message: str context: Optional[Dict[str, Any]] = {} history: Optional[List[Dict[str, Any]]] = [] @@ -2362,7 +2404,11 @@ async def claude_chat(request_data: ClaudeChatRequest): action="ai_chat", entity_type="chat", entity_id=model_name, - details={"message_length": len(message), "tools_enabled": enable_tools, "model": model_name} + details={ + "message_length": len(message), + "tools_enabled": enable_tools, + "model": model_name, + }, ) try: @@ -2376,11 +2422,11 @@ async def claude_chat(request_data: ClaudeChatRequest): return { "success": False, "response": f"{model_name.title()} is not available. Check API keys and SDK installation.", - "model": "error" + "model": "error", } # Import MCP handler - from dss_mcp.handler import get_mcp_handler, MCPContext + from dss_mcp.handler import MCPContext, get_mcp_handler mcp_handler = get_mcp_handler() @@ -2427,7 +2473,7 @@ CURRENT PROJECT CONTEXT: if "component" in context: context_parts.append(f"Component: {context['component']}") if context_parts: - system_prompt += f"\n\nUser context:\n" + "\n".join(context_parts) + system_prompt += "\n\nUser context:\n" + "\n".join(context_parts) # Get tools if enabled tools = None @@ -2435,10 +2481,7 @@ CURRENT PROJECT CONTEXT: tools = mcp_handler.get_tools_for_claude() # Create MCP context - mcp_context = MCPContext( - project_id=project_id, - user_id=user_id - ) + mcp_context = MCPContext(project_id=project_id, user_id=user_id) # Call AI provider with all context result = await provider.chat( @@ -2448,7 +2491,7 @@ CURRENT PROJECT CONTEXT: tools=tools, temperature=0.7, mcp_handler=mcp_handler, - mcp_context=mcp_context + mcp_context=mcp_context, ) # Log tool usage @@ -2458,7 +2501,7 @@ CURRENT PROJECT CONTEXT: entity_type="chat", entity_id=model_name, project_id=project_id, - details={"tools": result["tools_used"], "model": model_name} + details={"tools": result["tools_used"], "model": model_name}, ) return result @@ -2468,67 +2511,91 @@ CURRENT PROJECT CONTEXT: return { "success": False, "response": f"Error connecting to {model_name.title()}: {error_msg}\n\nMake sure your API key is valid and you have API access.", - "model": "error" + "model": "error", } # === MCP Tools Proxy === + @app.post("/api/mcp/{tool_name}") async def execute_mcp_tool(tool_name: str, params: Dict[str, Any] = {}): """ Proxy MCP tool execution. + Calls the MCP server running on port 3457. """ try: # Import MCP server functions from mcp_server import ( - get_status, list_projects, create_project, get_project, - extract_tokens, extract_components, generate_component_code, - sync_tokens_to_file, get_sync_history, get_activity, - ingest_css_tokens, ingest_scss_tokens, ingest_tailwind_tokens, - ingest_json_tokens, merge_tokens, export_tokens, validate_tokens, - discover_project, analyze_react_components, find_inline_styles, - find_style_patterns, analyze_style_values, find_unused_styles, - build_source_graph, get_quick_wins, get_quick_wins_report, - check_naming_consistency, scan_storybook, generate_story, - generate_stories_batch, generate_storybook_theme, get_story_coverage + analyze_react_components, + analyze_style_values, + build_source_graph, + check_naming_consistency, + create_project, + discover_project, + export_tokens, + extract_components, + extract_tokens, + find_inline_styles, + find_style_patterns, + find_unused_styles, + generate_component_code, + generate_stories_batch, + generate_story, + generate_storybook_theme, + get_activity, + get_project, + get_quick_wins, + get_quick_wins_report, + get_status, + get_story_coverage, + get_sync_history, + ingest_css_tokens, + ingest_json_tokens, + ingest_scss_tokens, + ingest_tailwind_tokens, + list_projects, + merge_tokens, + scan_storybook, + sync_tokens_to_file, + validate_tokens, ) # Map tool names to functions tool_map = { - 'get_status': get_status, - 'list_projects': list_projects, - 'create_project': create_project, - 'get_project': get_project, - 'extract_tokens': extract_tokens, - 'extract_components': extract_components, - 'generate_component_code': generate_component_code, - 'sync_tokens_to_file': sync_tokens_to_file, - 'get_sync_history': get_sync_history, - 'get_activity': get_activity, - 'ingest_css_tokens': ingest_css_tokens, - 'ingest_scss_tokens': ingest_scss_tokens, - 'ingest_tailwind_tokens': ingest_tailwind_tokens, - 'ingest_json_tokens': ingest_json_tokens, - 'merge_tokens': merge_tokens, - 'export_tokens': export_tokens, - 'validate_tokens': validate_tokens, - 'discover_project': discover_project, - 'analyze_react_components': analyze_react_components, - 'find_inline_styles': find_inline_styles, - 'find_style_patterns': find_style_patterns, - 'analyze_style_values': analyze_style_values, - 'find_unused_styles': find_unused_styles, - 'build_source_graph': build_source_graph, - 'get_quick_wins': get_quick_wins, - 'get_quick_wins_report': get_quick_wins_report, - 'check_naming_consistency': check_naming_consistency, - 'scan_storybook': scan_storybook, - 'generate_story': generate_story, - 'generate_stories_batch': generate_stories_batch, - 'generate_storybook_theme': generate_storybook_theme, - 'get_story_coverage': get_story_coverage, + "get_status": get_status, + "list_projects": list_projects, + "create_project": create_project, + "get_project": get_project, + "extract_tokens": extract_tokens, + "extract_components": extract_components, + "generate_component_code": generate_component_code, + "sync_tokens_to_file": sync_tokens_to_file, + "get_sync_history": get_sync_history, + "get_activity": get_activity, + "ingest_css_tokens": ingest_css_tokens, + "ingest_scss_tokens": ingest_scss_tokens, + "ingest_tailwind_tokens": ingest_tailwind_tokens, + "ingest_json_tokens": ingest_json_tokens, + "merge_tokens": merge_tokens, + "export_tokens": export_tokens, + "validate_tokens": validate_tokens, + "discover_project": discover_project, + "analyze_react_components": analyze_react_components, + "find_inline_styles": find_inline_styles, + "find_style_patterns": find_style_patterns, + "analyze_style_values": analyze_style_values, + "find_unused_styles": find_unused_styles, + "build_source_graph": build_source_graph, + "get_quick_wins": get_quick_wins, + "get_quick_wins_report": get_quick_wins_report, + "check_naming_consistency": check_naming_consistency, + "scan_storybook": scan_storybook, + "generate_story": generate_story, + "generate_stories_batch": generate_stories_batch, + "generate_storybook_theme": generate_storybook_theme, + "get_story_coverage": get_story_coverage, } # Get the tool function @@ -2544,7 +2611,7 @@ async def execute_mcp_tool(tool_name: str, params: Dict[str, Any] = {}): action="mcp_tool_executed", entity_type="tool", entity_id=tool_name, - details={"params": list(params.keys())} + details={"params": list(params.keys())}, ) return JSONResponse(content={"success": True, "result": result}) @@ -2554,22 +2621,25 @@ async def execute_mcp_tool(tool_name: str, params: Dict[str, Any] = {}): action="mcp_tool_failed", entity_type="tool", entity_id=tool_name, - details={"error": str(e)} + details={"error": str(e)}, ) raise HTTPException(status_code=500, detail=str(e)) # === MCP Integration Endpoints === + class IntegrationCreate(BaseModel): - """Create/Update integration configuration""" + """Create/Update integration configuration.""" + integration_type: str # figma, jira, confluence, sequential-thinking config: Dict[str, Any] # Encrypted in database enabled: bool = True class IntegrationUpdate(BaseModel): - """Update integration""" + """Update integration.""" + config: Optional[Dict[str, Any]] = None enabled: Optional[bool] = None @@ -2586,7 +2656,7 @@ async def list_all_integrations(): {"integration_type": "figma", "is_healthy": True, "failure_count": 0}, {"integration_type": "jira", "is_healthy": True, "failure_count": 0}, {"integration_type": "confluence", "is_healthy": True, "failure_count": 0}, - {"integration_type": "sequential-thinking", "is_healthy": True, "failure_count": 0} + {"integration_type": "sequential-thinking", "is_healthy": True, "failure_count": 0}, ] } @@ -2595,8 +2665,7 @@ async def list_all_integrations(): @app.get("/api/projects/{project_id}/integrations") async def list_project_integrations( - project_id: str, - user_id: Optional[int] = Query(None, description="Filter by user ID") + project_id: str, user_id: Optional[int] = Query(None, description="Filter by user ID") ): """List integrations configured for a project.""" if not Projects.get(project_id): @@ -2610,7 +2679,7 @@ async def list_project_integrations( async def create_integration( project_id: str, integration: IntegrationCreate, - user_id: int = Query(..., description="User ID for user-scoped integration") + user_id: int = Query(..., description="User ID for user-scoped integration"), ): """Create or update integration for a project (user-scoped).""" if not Projects.get(project_id): @@ -2632,7 +2701,7 @@ async def create_integration( user_id=user_id, integration_type=integration.integration_type, config=encrypted_config, - enabled=integration.enabled + enabled=integration.enabled, ) ActivityLog.log( @@ -2640,13 +2709,13 @@ async def create_integration( entity_type="integration", entity_id=integration.integration_type, project_id=project_id, - details={"user_id": user_id, "enabled": integration.enabled} + details={"user_id": user_id, "enabled": integration.enabled}, ) return { "success": True, "integration_type": integration.integration_type, - "enabled": integration.enabled + "enabled": integration.enabled, } except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @@ -2657,7 +2726,7 @@ async def update_integration( project_id: str, integration_type: str, update: IntegrationUpdate, - user_id: int = Query(..., description="User ID") + user_id: int = Query(..., description="User ID"), ): """Update an existing integration.""" if not Projects.get(project_id): @@ -2683,7 +2752,7 @@ async def update_integration( user_id=user_id, integration_type=integration_type, config=encrypted_config, - enabled=update.enabled + enabled=update.enabled, ) if not result: @@ -2698,9 +2767,7 @@ async def update_integration( @app.delete("/api/projects/{project_id}/integrations/{integration_type}") async def delete_integration( - project_id: str, - integration_type: str, - user_id: int = Query(..., description="User ID") + project_id: str, integration_type: str, user_id: int = Query(..., description="User ID") ): """Delete an integration configuration.""" if not Projects.get(project_id): @@ -2717,7 +2784,7 @@ async def delete_integration( entity_type="integration", entity_id=integration_type, project_id=project_id, - details={"user_id": user_id} + details={"user_id": user_id}, ) return {"success": True} @@ -2728,7 +2795,9 @@ async def delete_integration( @app.get("/api/mcp/tools") -async def list_mcp_tools(include_details: bool = Query(False, description="Include full tool schemas")): +async def list_mcp_tools( + include_details: bool = Query(False, description="Include full tool schemas"), +): """List all available MCP tools via unified handler.""" from dss_mcp.handler import get_mcp_handler @@ -2751,7 +2820,8 @@ async def get_mcp_tool_info(tool_name: str): class MCPToolExecuteRequest(BaseModel): - """Request to execute an MCP tool""" + """Request to execute an MCP tool.""" + arguments: Dict[str, Any] project_id: str user_id: Optional[int] = 1 @@ -2768,21 +2838,16 @@ async def execute_mcp_tool(tool_name: str, request: MCPToolExecuteRequest): - Applies circuit breaker protection - Logs execution metrics """ - from dss_mcp.handler import get_mcp_handler, MCPContext + from dss_mcp.handler import MCPContext, get_mcp_handler handler = get_mcp_handler() # Create execution context - context = MCPContext( - project_id=request.project_id, - user_id=request.user_id - ) + context = MCPContext(project_id=request.project_id, user_id=request.user_id) # Execute tool result = await handler.execute_tool( - tool_name=tool_name, - arguments=request.arguments, - context=context + tool_name=tool_name, arguments=request.arguments, context=context ) # Log to activity @@ -2794,8 +2859,8 @@ async def execute_mcp_tool(tool_name: str, request: MCPToolExecuteRequest): details={ "success": result.success, "duration_ms": result.duration_ms, - "error": result.error - } + "error": result.error, + }, ) return result.to_dict() @@ -2804,7 +2869,7 @@ async def execute_mcp_tool(tool_name: str, request: MCPToolExecuteRequest): @app.get("/api/mcp/status") async def get_mcp_status(): """Get MCP server status and configuration.""" - from dss_mcp.config import mcp_config, integration_config, validate_config + from dss_mcp.config import integration_config, mcp_config, validate_config warnings = validate_config() @@ -2813,24 +2878,25 @@ async def get_mcp_status(): "host": mcp_config.HOST, "port": mcp_config.PORT, "encryption_enabled": bool(mcp_config.ENCRYPTION_KEY), - "context_cache_ttl": mcp_config.CONTEXT_CACHE_TTL + "context_cache_ttl": mcp_config.CONTEXT_CACHE_TTL, }, "integrations": { "figma": bool(integration_config.FIGMA_TOKEN), "anthropic": bool(integration_config.ANTHROPIC_API_KEY), "jira_default": bool(integration_config.JIRA_URL), - "confluence_default": bool(integration_config.CONFLUENCE_URL) + "confluence_default": bool(integration_config.CONFLUENCE_URL), }, "circuit_breaker": { "failure_threshold": mcp_config.CIRCUIT_BREAKER_FAILURE_THRESHOLD, - "timeout_seconds": mcp_config.CIRCUIT_BREAKER_TIMEOUT_SECONDS + "timeout_seconds": mcp_config.CIRCUIT_BREAKER_TIMEOUT_SECONDS, }, - "warnings": warnings + "warnings": warnings, } # === MVP1: Project Configuration & Sandboxed File System === + @app.get("/api/projects/{project_id}/config") async def get_project_config(project_id: str): """Get project configuration from .dss/config.json.""" @@ -2838,7 +2904,7 @@ async def get_project_config(project_id: str): if not project: raise HTTPException(status_code=404, detail="Project not found") - root_path = project.get('root_path') + root_path = project.get("root_path") if not root_path: raise HTTPException(status_code=400, detail="Project has no root_path configured") @@ -2853,7 +2919,7 @@ async def update_project_config(project_id: str, updates: Dict[str, Any]): if not project: raise HTTPException(status_code=404, detail="Project not found") - root_path = project.get('root_path') + root_path = project.get("root_path") if not root_path: raise HTTPException(status_code=400, detail="Project has no root_path configured") @@ -2872,7 +2938,7 @@ async def get_project_context(project_id: str): if not project: raise HTTPException(status_code=404, detail="Project not found") - root_path = project.get('root_path') + root_path = project.get("root_path") if not root_path: raise HTTPException(status_code=400, detail="Project has no root_path configured") @@ -2891,14 +2957,10 @@ async def get_project_context(project_id: str): pass return { - "project": { - "id": project['id'], - "name": project['name'], - "root_path": root_path - }, + "project": {"id": project["id"], "name": project["name"], "root_path": root_path}, "config": config.dict(), "file_tree": fs.get_file_tree(max_depth=2), - "context_files": context_files + "context_files": context_files, } @@ -2909,7 +2971,7 @@ async def list_project_files(project_id: str, path: str = "."): if not project: raise HTTPException(status_code=404, detail="Project not found") - root_path = project.get('root_path') + root_path = project.get("root_path") if not root_path: raise HTTPException(status_code=400, detail="Project has no root_path configured") @@ -2929,7 +2991,7 @@ async def get_project_file_tree(project_id: str, max_depth: int = 3): if not project: raise HTTPException(status_code=404, detail="Project not found") - root_path = project.get('root_path') + root_path = project.get("root_path") if not root_path: raise HTTPException(status_code=400, detail="Project has no root_path configured") @@ -2944,7 +3006,7 @@ async def read_project_file(project_id: str, path: str): if not project: raise HTTPException(status_code=404, detail="Project not found") - root_path = project.get('root_path') + root_path = project.get("root_path") if not root_path: raise HTTPException(status_code=400, detail="Project has no root_path configured") @@ -2972,7 +3034,7 @@ async def write_project_file(project_id: str, request: FileWriteRequest): if not project: raise HTTPException(status_code=404, detail="Project not found") - root_path = project.get('root_path') + root_path = project.get("root_path") if not root_path: raise HTTPException(status_code=400, detail="Project has no root_path configured") @@ -2989,7 +3051,7 @@ async def write_project_file(project_id: str, request: FileWriteRequest): entity_type="file", entity_id=request.path, project_id=project_id, - details={"path": request.path, "size": len(request.content)} + details={"path": request.path, "size": len(request.content)}, ) return {"status": "ok", "path": request.path} except PermissionError as e: @@ -3005,13 +3067,11 @@ def kill_port(port: int, wait: float = 0.5) -> None: """Kill any process using the specified port.""" import subprocess import time + try: # Get PIDs using the port - result = subprocess.run( - ["lsof", "-ti", f":{port}"], - capture_output=True, text=True - ) - pids = result.stdout.strip().split('\n') + result = subprocess.run(["lsof", "-ti", f":{port}"], capture_output=True, text=True) + pids = result.stdout.strip().split("\n") killed = False for pid in pids: if pid: @@ -3035,7 +3095,8 @@ if __name__ == "__main__": kill_port(port, wait=0.5) url = f"http://{host}:{port}" - print(f""" + print( + f""" ╔═══════════════════════════════════════════════════════════════╗ ║ Design System Server (DSS) - Portable Server ║ ╠═══════════════════════════════════════════════════════════════╣ @@ -3045,11 +3106,7 @@ if __name__ == "__main__": ║ Environment: {config.server.env:^47}║ ║ Figma Mode: {figma_suite.mode:^47}║ ╚═══════════════════════════════════════════════════════════════╝ -""") - - uvicorn.run( - "server:app", - host=host, - port=port, - reload=config.server.env == "development" +""" ) + + uvicorn.run("server:app", host=host, port=port, reload=config.server.env == "development") diff --git a/apps/cli/python/api/server.py b/apps/cli/python/api/server.py index c13bffb..a6a2771 100644 --- a/apps/cli/python/api/server.py +++ b/apps/cli/python/api/server.py @@ -1,5 +1,5 @@ """ -Design System Server (DSS) - FastAPI Server +Design System Server (DSS) - FastAPI Server. Portable API server providing: - Project management (CRUD) @@ -16,37 +16,43 @@ Modes: Uses SQLite for persistence, integrates with Figma tools. """ -import asyncio -import subprocess import json import os -from pathlib import Path -from typing import Optional, List, Dict, Any -from datetime import datetime - -from fastapi import FastAPI, HTTPException, Query, BackgroundTasks -from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import JSONResponse -from fastapi.staticfiles import StaticFiles -from pydantic import BaseModel - +import subprocess import sys -sys.path.insert(0, str(Path(__file__).parent.parent)) +from datetime import datetime +from pathlib import Path +from typing import Any, Dict, Optional from config import config -from storage.json_store import ( - Projects, Components, SyncHistory, ActivityLog, Teams, Cache, get_stats -) +from fastapi import BackgroundTasks, FastAPI, HTTPException, Query +from fastapi.middleware.cors import CORSMiddleware +from fastapi.staticfiles import StaticFiles from figma.figma_tools import FigmaToolSuite +from pydantic import BaseModel +from storage.json_store import ( + ActivityLog, + Cache, + Components, + Projects, + SyncHistory, + Teams, + get_stats, +) + +sys.path.insert(0, str(Path(__file__).parent.parent)) # === Runtime Configuration === + class RuntimeConfig: """ Runtime configuration that can be modified from the dashboard. + Persists to .dss/runtime-config.json for portability. """ + def __init__(self): self.config_path = Path(__file__).parent.parent.parent / ".dss" / "runtime-config.json" self.config_path.parent.mkdir(parents=True, exist_ok=True) @@ -71,7 +77,7 @@ class RuntimeConfig: "token_sync": True, "code_gen": True, "ai_advisor": False, - } + }, } def _save(self): @@ -114,6 +120,7 @@ runtime_config = RuntimeConfig() # === Service Discovery === + class ServiceDiscovery: """Discovers and manages companion services.""" @@ -136,13 +143,13 @@ class ServiceDiscovery: try: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(0.5) - result = sock.connect_ex(('127.0.0.1', port)) + result = sock.connect_ex(("127.0.0.1", port)) sock.close() if result == 0: discovered[service] = { "running": True, "port": port, - "url": f"http://localhost:{port}" + "url": f"http://localhost:{port}", } break except: @@ -164,11 +171,7 @@ class ServiceDiscovery: try: async with httpx.AsyncClient(timeout=2.0) as client: resp = await client.get(url) - return { - "running": resp.status_code == 200, - "url": url, - "port": port - } + return {"running": resp.status_code == 200, "url": url, "port": port} except: return {"running": False, "url": url, "port": port} @@ -178,7 +181,7 @@ class ServiceDiscovery: app = FastAPI( title="Design System Server (DSS)", description="API for design system management and Figma integration", - version="1.0.0" + version="1.0.0", ) app.add_middleware( @@ -195,31 +198,38 @@ if UI_DIR.exists(): app.mount("/admin-ui", StaticFiles(directory=str(UI_DIR), html=True), name="admin-ui") # Initialize Figma tools -figma_suite = FigmaToolSuite(output_dir=str(Path(__file__).parent.parent.parent / ".dss" / "output")) +figma_suite = FigmaToolSuite( + output_dir=str(Path(__file__).parent.parent.parent / ".dss" / "output") +) # === Request/Response Models === + class ProjectCreate(BaseModel): name: str description: str = "" figma_file_key: str = "" + class ProjectUpdate(BaseModel): name: Optional[str] = None description: Optional[str] = None figma_file_key: Optional[str] = None status: Optional[str] = None + class FigmaExtractRequest(BaseModel): file_key: str format: str = "css" + class FigmaSyncRequest(BaseModel): file_key: str target_path: str format: str = "css" + class TeamCreate(BaseModel): name: str description: str = "" @@ -227,10 +237,12 @@ class TeamCreate(BaseModel): # === Root & Health === + @app.get("/") async def root(): """Redirect to Admin UI dashboard.""" from fastapi.responses import RedirectResponse + return RedirectResponse(url="/admin-ui/index.html") @@ -243,30 +255,30 @@ async def health(): "version": "1.0.0", "timestamp": datetime.utcnow().isoformat() + "Z", "figma_mode": figma_suite.mode, - "config": config.summary() + "config": config.summary(), } + @app.get("/api/stats") async def get_statistics(): """Get database and system statistics.""" db_stats = get_stats() return { "database": db_stats, - "figma": { - "mode": figma_suite.mode, - "configured": config.figma.is_configured - } + "figma": {"mode": figma_suite.mode, "configured": config.figma.is_configured}, } # === Projects === + @app.get("/api/projects") async def list_projects(status: Optional[str] = None): """List all projects.""" projects = Projects.list(status=status) return projects + @app.get("/api/projects/{project_id}") async def get_project(project_id: str): """Get a specific project.""" @@ -275,6 +287,7 @@ async def get_project(project_id: str): raise HTTPException(status_code=404, detail="Project not found") return project + @app.post("/api/projects") async def create_project(project: ProjectCreate): """Create a new project.""" @@ -283,17 +296,18 @@ async def create_project(project: ProjectCreate): id=project_id, name=project.name, description=project.description, - figma_file_key=project.figma_file_key + figma_file_key=project.figma_file_key, ) ActivityLog.log( action="project_created", entity_type="project", entity_id=project_id, project_id=project_id, - details={"name": project.name} + details={"name": project.name}, ) return created + @app.put("/api/projects/{project_id}") async def update_project(project_id: str, update: ProjectUpdate): """Update a project.""" @@ -311,25 +325,23 @@ async def update_project(project_id: str, update: ProjectUpdate): entity_type="project", entity_id=project_id, project_id=project_id, - details=update_data + details=update_data, ) return updated + @app.delete("/api/projects/{project_id}") async def delete_project(project_id: str): """Delete a project.""" if not Projects.delete(project_id): raise HTTPException(status_code=404, detail="Project not found") - ActivityLog.log( - action="project_deleted", - entity_type="project", - entity_id=project_id - ) + ActivityLog.log(action="project_deleted", entity_type="project", entity_id=project_id) return {"success": True} # === Components === + @app.get("/api/projects/{project_id}/components") async def list_components(project_id: str): """List components for a project.""" @@ -340,6 +352,7 @@ async def list_components(project_id: str): # === Figma Integration === + @app.post("/api/figma/extract-variables") async def extract_variables(request: FigmaExtractRequest, background_tasks: BackgroundTasks): """Extract design tokens from Figma file.""" @@ -348,12 +361,17 @@ async def extract_variables(request: FigmaExtractRequest, background_tasks: Back ActivityLog.log( action="figma_extract_variables", entity_type="figma", - details={"file_key": request.file_key, "format": request.format, "count": result.get("tokens_count")} + details={ + "file_key": request.file_key, + "format": request.format, + "count": result.get("tokens_count"), + }, ) return result except Exception as e: raise HTTPException(status_code=500, detail=str(e)) + @app.post("/api/figma/extract-components") async def extract_components(request: FigmaExtractRequest): """Extract components from Figma file.""" @@ -362,12 +380,13 @@ async def extract_components(request: FigmaExtractRequest): ActivityLog.log( action="figma_extract_components", entity_type="figma", - details={"file_key": request.file_key, "count": result.get("components_count")} + details={"file_key": request.file_key, "count": result.get("components_count")}, ) return result except Exception as e: raise HTTPException(status_code=500, detail=str(e)) + @app.post("/api/figma/extract-styles") async def extract_styles(request: FigmaExtractRequest): """Extract styles from Figma file.""" @@ -377,20 +396,28 @@ async def extract_styles(request: FigmaExtractRequest): except Exception as e: raise HTTPException(status_code=500, detail=str(e)) + @app.post("/api/figma/sync-tokens") async def sync_tokens(request: FigmaSyncRequest): """Sync tokens from Figma to target path.""" try: - result = await figma_suite.sync_tokens(request.file_key, request.target_path, request.format) + result = await figma_suite.sync_tokens( + request.file_key, request.target_path, request.format + ) ActivityLog.log( action="figma_sync_tokens", entity_type="figma", - details={"file_key": request.file_key, "target": request.target_path, "synced": result.get("tokens_synced")} + details={ + "file_key": request.file_key, + "target": request.target_path, + "synced": result.get("tokens_synced"), + }, ) return result except Exception as e: raise HTTPException(status_code=500, detail=str(e)) + @app.post("/api/figma/validate") async def validate_components(request: FigmaExtractRequest): """Validate components against design system rules.""" @@ -400,6 +427,7 @@ async def validate_components(request: FigmaExtractRequest): except Exception as e: raise HTTPException(status_code=500, detail=str(e)) + @app.post("/api/figma/generate-code") async def generate_code(file_key: str, component_name: str, framework: str = "webcomponent"): """Generate component code from Figma.""" @@ -412,6 +440,7 @@ async def generate_code(file_key: str, component_name: str, framework: str = "we # === Discovery === + @app.get("/api/discovery") async def run_discovery(path: str = "."): """Run project discovery.""" @@ -419,10 +448,7 @@ async def run_discovery(path: str = "."): try: result = subprocess.run( - [str(script_path), path], - capture_output=True, - text=True, - timeout=30 + [str(script_path), path], capture_output=True, text=True, timeout=30 ) if result.returncode == 0: return json.loads(result.stdout) @@ -433,22 +459,19 @@ async def run_discovery(path: str = "."): except json.JSONDecodeError: return {"raw_output": result.stdout} + @app.get("/api/discovery/ports") async def discover_ports(): """Discover listening ports and services.""" script_path = Path(__file__).parent.parent / "discovery" / "discover-ports.sh" try: - result = subprocess.run( - [str(script_path)], - capture_output=True, - text=True, - timeout=10 - ) + result = subprocess.run([str(script_path)], capture_output=True, text=True, timeout=10) return json.loads(result.stdout) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) + @app.get("/api/discovery/env") async def discover_env(path: str = "."): """Analyze environment configuration.""" @@ -456,10 +479,7 @@ async def discover_env(path: str = "."): try: result = subprocess.run( - [str(script_path), path], - capture_output=True, - text=True, - timeout=10 + [str(script_path), path], capture_output=True, text=True, timeout=10 ) return json.loads(result.stdout) except Exception as e: @@ -468,24 +488,30 @@ async def discover_env(path: str = "."): # === Activity & Sync History === + @app.get("/api/activity") async def get_activity(limit: int = Query(default=50, le=100)): """Get recent activity log.""" return ActivityLog.recent(limit=limit) + @app.get("/api/sync-history") -async def get_sync_history(project_id: Optional[str] = None, limit: int = Query(default=20, le=100)): +async def get_sync_history( + project_id: Optional[str] = None, limit: int = Query(default=20, le=100) +): """Get sync history.""" return SyncHistory.recent(project_id=project_id, limit=limit) # === Teams === + @app.get("/api/teams") async def list_teams(): """List all teams.""" return Teams.list() + @app.post("/api/teams") async def create_team(team: TeamCreate): """Create a new team.""" @@ -493,6 +519,7 @@ async def create_team(team: TeamCreate): created = Teams.create(team_id, team.name, team.description) return created + @app.get("/api/teams/{team_id}") async def get_team(team_id: str): """Get a specific team.""" @@ -504,12 +531,14 @@ async def get_team(team_id: str): # === Cache Management === + @app.post("/api/cache/clear") async def clear_cache(): """Clear expired cache entries.""" count = Cache.clear_expired() return {"cleared": count} + @app.delete("/api/cache") async def purge_cache(): """Purge all cache entries.""" @@ -519,6 +548,7 @@ async def purge_cache(): # === Configuration Management === + class ConfigUpdate(BaseModel): mode: Optional[str] = None figma_token: Optional[str] = None @@ -532,7 +562,7 @@ async def get_config(): return { "config": runtime_config.get(), "env": config.summary(), - "mode": runtime_config.get("mode") + "mode": runtime_config.get("mode"), } @@ -548,11 +578,13 @@ async def update_config(update: ConfigUpdate): runtime_config.set_figma_token(update.figma_token) # Reinitialize Figma tools with new token global figma_suite - figma_suite = FigmaToolSuite(output_dir=str(Path(__file__).parent.parent.parent / ".dss" / "output")) + figma_suite = FigmaToolSuite( + output_dir=str(Path(__file__).parent.parent.parent / ".dss" / "output") + ) ActivityLog.log( action="figma_token_updated", entity_type="config", - details={"configured": bool(update.figma_token)} + details={"configured": bool(update.figma_token)}, ) if update.services: @@ -564,9 +596,7 @@ async def update_config(update: ConfigUpdate): if updates: runtime_config.update(updates) ActivityLog.log( - action="config_updated", - entity_type="config", - details={"keys": list(updates.keys())} + action="config_updated", entity_type="config", details={"keys": list(updates.keys())} ) return runtime_config.get() @@ -586,7 +616,7 @@ async def get_figma_config(): "sync_tokens": True, "validate": True, "generate_code": True, - } + }, } @@ -600,18 +630,16 @@ async def test_figma_connection(): # Test with a minimal API call import httpx + token = runtime_config._data["figma"]["token"] async with httpx.AsyncClient() as client: - resp = await client.get( - "https://api.figma.com/v1/me", - headers={"X-Figma-Token": token} - ) + resp = await client.get("https://api.figma.com/v1/me", headers={"X-Figma-Token": token}) if resp.status_code == 200: user = resp.json() return { "success": True, "user": user.get("email", "connected"), - "handle": user.get("handle") + "handle": user.get("handle"), } else: return {"success": False, "error": f"API returned {resp.status_code}"} @@ -621,6 +649,7 @@ async def test_figma_connection(): # === Service Discovery === + @app.get("/api/services") async def list_services(): """List configured and discovered services.""" @@ -630,7 +659,7 @@ async def list_services(): return { "configured": configured, "discovered": discovered, - "storybook": await ServiceDiscovery.check_storybook() + "storybook": await ServiceDiscovery.check_storybook(), } @@ -645,7 +674,7 @@ async def configure_service(service_name: str, config_data: Dict[str, Any]): action="service_configured", entity_type="service", entity_id=service_name, - details={"keys": list(config_data.keys())} + details={"keys": list(config_data.keys())}, ) return services[service_name] @@ -659,6 +688,7 @@ async def get_storybook_status(): # === DSS Mode === + @app.get("/api/mode") async def get_mode(): """Get current DSS mode.""" @@ -666,7 +696,7 @@ async def get_mode(): return { "mode": mode, "description": "Local dev companion" if mode == "local" else "Remote design system server", - "features": runtime_config.get("features") + "features": runtime_config.get("features"), } @@ -677,11 +707,7 @@ async def set_mode(mode: str): raise HTTPException(status_code=400, detail="Mode must be 'local' or 'server'") runtime_config.set("mode", mode) - ActivityLog.log( - action="mode_changed", - entity_type="config", - details={"mode": mode} - ) + ActivityLog.log(action="mode_changed", entity_type="config", details={"mode": mode}) return {"mode": mode, "success": True} @@ -704,7 +730,8 @@ if __name__ == "__main__": host = os.getenv("HOST", "0.0.0.0") url = f"http://{host}:{port}" - print(f""" + print( + f""" ╔═══════════════════════════════════════════════════════════════╗ ║ Design System Server (DSS) - Portable Server ║ ╠═══════════════════════════════════════════════════════════════╣ @@ -714,11 +741,7 @@ if __name__ == "__main__": ║ Environment: {config.server.env:^47}║ ║ Figma Mode: {figma_suite.mode:^47}║ ╚═══════════════════════════════════════════════════════════════╝ -""") - - uvicorn.run( - "server:app", - host=host, - port=port, - reload=config.server.env == "development" +""" ) + + uvicorn.run("server:app", host=host, port=port, reload=config.server.env == "development") diff --git a/cli/python/api/server.py b/cli/python/api/server.py index c13bffb..a6a2771 100644 --- a/cli/python/api/server.py +++ b/cli/python/api/server.py @@ -1,5 +1,5 @@ """ -Design System Server (DSS) - FastAPI Server +Design System Server (DSS) - FastAPI Server. Portable API server providing: - Project management (CRUD) @@ -16,37 +16,43 @@ Modes: Uses SQLite for persistence, integrates with Figma tools. """ -import asyncio -import subprocess import json import os -from pathlib import Path -from typing import Optional, List, Dict, Any -from datetime import datetime - -from fastapi import FastAPI, HTTPException, Query, BackgroundTasks -from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import JSONResponse -from fastapi.staticfiles import StaticFiles -from pydantic import BaseModel - +import subprocess import sys -sys.path.insert(0, str(Path(__file__).parent.parent)) +from datetime import datetime +from pathlib import Path +from typing import Any, Dict, Optional from config import config -from storage.json_store import ( - Projects, Components, SyncHistory, ActivityLog, Teams, Cache, get_stats -) +from fastapi import BackgroundTasks, FastAPI, HTTPException, Query +from fastapi.middleware.cors import CORSMiddleware +from fastapi.staticfiles import StaticFiles from figma.figma_tools import FigmaToolSuite +from pydantic import BaseModel +from storage.json_store import ( + ActivityLog, + Cache, + Components, + Projects, + SyncHistory, + Teams, + get_stats, +) + +sys.path.insert(0, str(Path(__file__).parent.parent)) # === Runtime Configuration === + class RuntimeConfig: """ Runtime configuration that can be modified from the dashboard. + Persists to .dss/runtime-config.json for portability. """ + def __init__(self): self.config_path = Path(__file__).parent.parent.parent / ".dss" / "runtime-config.json" self.config_path.parent.mkdir(parents=True, exist_ok=True) @@ -71,7 +77,7 @@ class RuntimeConfig: "token_sync": True, "code_gen": True, "ai_advisor": False, - } + }, } def _save(self): @@ -114,6 +120,7 @@ runtime_config = RuntimeConfig() # === Service Discovery === + class ServiceDiscovery: """Discovers and manages companion services.""" @@ -136,13 +143,13 @@ class ServiceDiscovery: try: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(0.5) - result = sock.connect_ex(('127.0.0.1', port)) + result = sock.connect_ex(("127.0.0.1", port)) sock.close() if result == 0: discovered[service] = { "running": True, "port": port, - "url": f"http://localhost:{port}" + "url": f"http://localhost:{port}", } break except: @@ -164,11 +171,7 @@ class ServiceDiscovery: try: async with httpx.AsyncClient(timeout=2.0) as client: resp = await client.get(url) - return { - "running": resp.status_code == 200, - "url": url, - "port": port - } + return {"running": resp.status_code == 200, "url": url, "port": port} except: return {"running": False, "url": url, "port": port} @@ -178,7 +181,7 @@ class ServiceDiscovery: app = FastAPI( title="Design System Server (DSS)", description="API for design system management and Figma integration", - version="1.0.0" + version="1.0.0", ) app.add_middleware( @@ -195,31 +198,38 @@ if UI_DIR.exists(): app.mount("/admin-ui", StaticFiles(directory=str(UI_DIR), html=True), name="admin-ui") # Initialize Figma tools -figma_suite = FigmaToolSuite(output_dir=str(Path(__file__).parent.parent.parent / ".dss" / "output")) +figma_suite = FigmaToolSuite( + output_dir=str(Path(__file__).parent.parent.parent / ".dss" / "output") +) # === Request/Response Models === + class ProjectCreate(BaseModel): name: str description: str = "" figma_file_key: str = "" + class ProjectUpdate(BaseModel): name: Optional[str] = None description: Optional[str] = None figma_file_key: Optional[str] = None status: Optional[str] = None + class FigmaExtractRequest(BaseModel): file_key: str format: str = "css" + class FigmaSyncRequest(BaseModel): file_key: str target_path: str format: str = "css" + class TeamCreate(BaseModel): name: str description: str = "" @@ -227,10 +237,12 @@ class TeamCreate(BaseModel): # === Root & Health === + @app.get("/") async def root(): """Redirect to Admin UI dashboard.""" from fastapi.responses import RedirectResponse + return RedirectResponse(url="/admin-ui/index.html") @@ -243,30 +255,30 @@ async def health(): "version": "1.0.0", "timestamp": datetime.utcnow().isoformat() + "Z", "figma_mode": figma_suite.mode, - "config": config.summary() + "config": config.summary(), } + @app.get("/api/stats") async def get_statistics(): """Get database and system statistics.""" db_stats = get_stats() return { "database": db_stats, - "figma": { - "mode": figma_suite.mode, - "configured": config.figma.is_configured - } + "figma": {"mode": figma_suite.mode, "configured": config.figma.is_configured}, } # === Projects === + @app.get("/api/projects") async def list_projects(status: Optional[str] = None): """List all projects.""" projects = Projects.list(status=status) return projects + @app.get("/api/projects/{project_id}") async def get_project(project_id: str): """Get a specific project.""" @@ -275,6 +287,7 @@ async def get_project(project_id: str): raise HTTPException(status_code=404, detail="Project not found") return project + @app.post("/api/projects") async def create_project(project: ProjectCreate): """Create a new project.""" @@ -283,17 +296,18 @@ async def create_project(project: ProjectCreate): id=project_id, name=project.name, description=project.description, - figma_file_key=project.figma_file_key + figma_file_key=project.figma_file_key, ) ActivityLog.log( action="project_created", entity_type="project", entity_id=project_id, project_id=project_id, - details={"name": project.name} + details={"name": project.name}, ) return created + @app.put("/api/projects/{project_id}") async def update_project(project_id: str, update: ProjectUpdate): """Update a project.""" @@ -311,25 +325,23 @@ async def update_project(project_id: str, update: ProjectUpdate): entity_type="project", entity_id=project_id, project_id=project_id, - details=update_data + details=update_data, ) return updated + @app.delete("/api/projects/{project_id}") async def delete_project(project_id: str): """Delete a project.""" if not Projects.delete(project_id): raise HTTPException(status_code=404, detail="Project not found") - ActivityLog.log( - action="project_deleted", - entity_type="project", - entity_id=project_id - ) + ActivityLog.log(action="project_deleted", entity_type="project", entity_id=project_id) return {"success": True} # === Components === + @app.get("/api/projects/{project_id}/components") async def list_components(project_id: str): """List components for a project.""" @@ -340,6 +352,7 @@ async def list_components(project_id: str): # === Figma Integration === + @app.post("/api/figma/extract-variables") async def extract_variables(request: FigmaExtractRequest, background_tasks: BackgroundTasks): """Extract design tokens from Figma file.""" @@ -348,12 +361,17 @@ async def extract_variables(request: FigmaExtractRequest, background_tasks: Back ActivityLog.log( action="figma_extract_variables", entity_type="figma", - details={"file_key": request.file_key, "format": request.format, "count": result.get("tokens_count")} + details={ + "file_key": request.file_key, + "format": request.format, + "count": result.get("tokens_count"), + }, ) return result except Exception as e: raise HTTPException(status_code=500, detail=str(e)) + @app.post("/api/figma/extract-components") async def extract_components(request: FigmaExtractRequest): """Extract components from Figma file.""" @@ -362,12 +380,13 @@ async def extract_components(request: FigmaExtractRequest): ActivityLog.log( action="figma_extract_components", entity_type="figma", - details={"file_key": request.file_key, "count": result.get("components_count")} + details={"file_key": request.file_key, "count": result.get("components_count")}, ) return result except Exception as e: raise HTTPException(status_code=500, detail=str(e)) + @app.post("/api/figma/extract-styles") async def extract_styles(request: FigmaExtractRequest): """Extract styles from Figma file.""" @@ -377,20 +396,28 @@ async def extract_styles(request: FigmaExtractRequest): except Exception as e: raise HTTPException(status_code=500, detail=str(e)) + @app.post("/api/figma/sync-tokens") async def sync_tokens(request: FigmaSyncRequest): """Sync tokens from Figma to target path.""" try: - result = await figma_suite.sync_tokens(request.file_key, request.target_path, request.format) + result = await figma_suite.sync_tokens( + request.file_key, request.target_path, request.format + ) ActivityLog.log( action="figma_sync_tokens", entity_type="figma", - details={"file_key": request.file_key, "target": request.target_path, "synced": result.get("tokens_synced")} + details={ + "file_key": request.file_key, + "target": request.target_path, + "synced": result.get("tokens_synced"), + }, ) return result except Exception as e: raise HTTPException(status_code=500, detail=str(e)) + @app.post("/api/figma/validate") async def validate_components(request: FigmaExtractRequest): """Validate components against design system rules.""" @@ -400,6 +427,7 @@ async def validate_components(request: FigmaExtractRequest): except Exception as e: raise HTTPException(status_code=500, detail=str(e)) + @app.post("/api/figma/generate-code") async def generate_code(file_key: str, component_name: str, framework: str = "webcomponent"): """Generate component code from Figma.""" @@ -412,6 +440,7 @@ async def generate_code(file_key: str, component_name: str, framework: str = "we # === Discovery === + @app.get("/api/discovery") async def run_discovery(path: str = "."): """Run project discovery.""" @@ -419,10 +448,7 @@ async def run_discovery(path: str = "."): try: result = subprocess.run( - [str(script_path), path], - capture_output=True, - text=True, - timeout=30 + [str(script_path), path], capture_output=True, text=True, timeout=30 ) if result.returncode == 0: return json.loads(result.stdout) @@ -433,22 +459,19 @@ async def run_discovery(path: str = "."): except json.JSONDecodeError: return {"raw_output": result.stdout} + @app.get("/api/discovery/ports") async def discover_ports(): """Discover listening ports and services.""" script_path = Path(__file__).parent.parent / "discovery" / "discover-ports.sh" try: - result = subprocess.run( - [str(script_path)], - capture_output=True, - text=True, - timeout=10 - ) + result = subprocess.run([str(script_path)], capture_output=True, text=True, timeout=10) return json.loads(result.stdout) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) + @app.get("/api/discovery/env") async def discover_env(path: str = "."): """Analyze environment configuration.""" @@ -456,10 +479,7 @@ async def discover_env(path: str = "."): try: result = subprocess.run( - [str(script_path), path], - capture_output=True, - text=True, - timeout=10 + [str(script_path), path], capture_output=True, text=True, timeout=10 ) return json.loads(result.stdout) except Exception as e: @@ -468,24 +488,30 @@ async def discover_env(path: str = "."): # === Activity & Sync History === + @app.get("/api/activity") async def get_activity(limit: int = Query(default=50, le=100)): """Get recent activity log.""" return ActivityLog.recent(limit=limit) + @app.get("/api/sync-history") -async def get_sync_history(project_id: Optional[str] = None, limit: int = Query(default=20, le=100)): +async def get_sync_history( + project_id: Optional[str] = None, limit: int = Query(default=20, le=100) +): """Get sync history.""" return SyncHistory.recent(project_id=project_id, limit=limit) # === Teams === + @app.get("/api/teams") async def list_teams(): """List all teams.""" return Teams.list() + @app.post("/api/teams") async def create_team(team: TeamCreate): """Create a new team.""" @@ -493,6 +519,7 @@ async def create_team(team: TeamCreate): created = Teams.create(team_id, team.name, team.description) return created + @app.get("/api/teams/{team_id}") async def get_team(team_id: str): """Get a specific team.""" @@ -504,12 +531,14 @@ async def get_team(team_id: str): # === Cache Management === + @app.post("/api/cache/clear") async def clear_cache(): """Clear expired cache entries.""" count = Cache.clear_expired() return {"cleared": count} + @app.delete("/api/cache") async def purge_cache(): """Purge all cache entries.""" @@ -519,6 +548,7 @@ async def purge_cache(): # === Configuration Management === + class ConfigUpdate(BaseModel): mode: Optional[str] = None figma_token: Optional[str] = None @@ -532,7 +562,7 @@ async def get_config(): return { "config": runtime_config.get(), "env": config.summary(), - "mode": runtime_config.get("mode") + "mode": runtime_config.get("mode"), } @@ -548,11 +578,13 @@ async def update_config(update: ConfigUpdate): runtime_config.set_figma_token(update.figma_token) # Reinitialize Figma tools with new token global figma_suite - figma_suite = FigmaToolSuite(output_dir=str(Path(__file__).parent.parent.parent / ".dss" / "output")) + figma_suite = FigmaToolSuite( + output_dir=str(Path(__file__).parent.parent.parent / ".dss" / "output") + ) ActivityLog.log( action="figma_token_updated", entity_type="config", - details={"configured": bool(update.figma_token)} + details={"configured": bool(update.figma_token)}, ) if update.services: @@ -564,9 +596,7 @@ async def update_config(update: ConfigUpdate): if updates: runtime_config.update(updates) ActivityLog.log( - action="config_updated", - entity_type="config", - details={"keys": list(updates.keys())} + action="config_updated", entity_type="config", details={"keys": list(updates.keys())} ) return runtime_config.get() @@ -586,7 +616,7 @@ async def get_figma_config(): "sync_tokens": True, "validate": True, "generate_code": True, - } + }, } @@ -600,18 +630,16 @@ async def test_figma_connection(): # Test with a minimal API call import httpx + token = runtime_config._data["figma"]["token"] async with httpx.AsyncClient() as client: - resp = await client.get( - "https://api.figma.com/v1/me", - headers={"X-Figma-Token": token} - ) + resp = await client.get("https://api.figma.com/v1/me", headers={"X-Figma-Token": token}) if resp.status_code == 200: user = resp.json() return { "success": True, "user": user.get("email", "connected"), - "handle": user.get("handle") + "handle": user.get("handle"), } else: return {"success": False, "error": f"API returned {resp.status_code}"} @@ -621,6 +649,7 @@ async def test_figma_connection(): # === Service Discovery === + @app.get("/api/services") async def list_services(): """List configured and discovered services.""" @@ -630,7 +659,7 @@ async def list_services(): return { "configured": configured, "discovered": discovered, - "storybook": await ServiceDiscovery.check_storybook() + "storybook": await ServiceDiscovery.check_storybook(), } @@ -645,7 +674,7 @@ async def configure_service(service_name: str, config_data: Dict[str, Any]): action="service_configured", entity_type="service", entity_id=service_name, - details={"keys": list(config_data.keys())} + details={"keys": list(config_data.keys())}, ) return services[service_name] @@ -659,6 +688,7 @@ async def get_storybook_status(): # === DSS Mode === + @app.get("/api/mode") async def get_mode(): """Get current DSS mode.""" @@ -666,7 +696,7 @@ async def get_mode(): return { "mode": mode, "description": "Local dev companion" if mode == "local" else "Remote design system server", - "features": runtime_config.get("features") + "features": runtime_config.get("features"), } @@ -677,11 +707,7 @@ async def set_mode(mode: str): raise HTTPException(status_code=400, detail="Mode must be 'local' or 'server'") runtime_config.set("mode", mode) - ActivityLog.log( - action="mode_changed", - entity_type="config", - details={"mode": mode} - ) + ActivityLog.log(action="mode_changed", entity_type="config", details={"mode": mode}) return {"mode": mode, "success": True} @@ -704,7 +730,8 @@ if __name__ == "__main__": host = os.getenv("HOST", "0.0.0.0") url = f"http://{host}:{port}" - print(f""" + print( + f""" ╔═══════════════════════════════════════════════════════════════╗ ║ Design System Server (DSS) - Portable Server ║ ╠═══════════════════════════════════════════════════════════════╣ @@ -714,11 +741,7 @@ if __name__ == "__main__": ║ Environment: {config.server.env:^47}║ ║ Figma Mode: {figma_suite.mode:^47}║ ╚═══════════════════════════════════════════════════════════════╝ -""") - - uvicorn.run( - "server:app", - host=host, - port=port, - reload=config.server.env == "development" +""" ) + + uvicorn.run("server:app", host=host, port=port, reload=config.server.env == "development") diff --git a/dss-claude-plugin/core/__init__.py b/dss-claude-plugin/core/__init__.py index caf5e92..803753d 100644 --- a/dss-claude-plugin/core/__init__.py +++ b/dss-claude-plugin/core/__init__.py @@ -1,19 +1,20 @@ """ -DSS Core Module - Configuration and Context Management +DSS Core Module - Configuration and Context Management. + Extended with Context Compiler for design system context resolution. """ +from .compiler import EMERGENCY_SKIN, ContextCompiler from .config import DSSConfig, DSSMode from .context import DSSContext -from .compiler import ContextCompiler, EMERGENCY_SKIN from .mcp_extensions import ( + COMPILER, get_active_context, + get_compiler_status, + list_skins, resolve_token, validate_manifest, - list_skins, - get_compiler_status, with_context, - COMPILER ) __all__ = [ @@ -28,5 +29,5 @@ __all__ = [ "list_skins", "get_compiler_status", "with_context", - "COMPILER" + "COMPILER", ] diff --git a/dss-claude-plugin/core/compiler.py b/dss-claude-plugin/core/compiler.py index 633e9cc..c61a5c1 100644 --- a/dss-claude-plugin/core/compiler.py +++ b/dss-claude-plugin/core/compiler.py @@ -1,16 +1,16 @@ """ -DSS Context Compiler +DSS Context Compiler. + Resolves project context via 3-layer cascade: Base -> Skin -> Project Includes Safe Boot Protocol and Debug Provenance. """ -import json -import os import copy +import json import logging from datetime import datetime, timezone -from typing import Dict, Any, Optional, List, Union from pathlib import Path +from typing import Any, Dict, List # Setup logging logging.basicConfig(level=logging.INFO) @@ -21,25 +21,26 @@ logger = logging.getLogger("DSSCompiler") EMERGENCY_SKIN = { "meta": {"id": "emergency", "version": "1.0.0"}, "tokens": { - "colors": { - "primary": "#FF0000", - "background": "#FFFFFF", - "text": "#000000" - }, - "spacing": {"base": "4px"} + "colors": {"primary": "#FF0000", "background": "#FFFFFF", "text": "#000000"}, + "spacing": {"base": "4px"}, }, - "status": "emergency_mode" + "status": "emergency_mode", } + class ContextCompiler: def __init__(self, skins_dir: str = "./skins"): self.skins_dir = Path(skins_dir) self.cache: Dict[str, Any] = {} self._manifest_mtimes: Dict[str, float] = {} # Track file modification times - def compile(self, manifest_path: str, debug: bool = False, force_refresh: bool = False) -> Dict[str, Any]: + def compile( + self, manifest_path: str, debug: bool = False, force_refresh: bool = False + ) -> Dict[str, Any]: """ - Main entry point. Compiles context by merging: + Main entry point. + + Compiles context by merging: 1. Base Skin (Implicit or Explicit) 2. Extended Skin (defined in manifest) 3. Project Overrides (defined in manifest) @@ -83,17 +84,17 @@ class ContextCompiler: # Merge Result + Project Overrides # Need to wrap project overrides in same structure as skins - project_overrides_wrapped = { - "tokens": manifest.get("overrides", {}).get("tokens", {}) - } - final_context = self._deep_merge(context, project_overrides_wrapped, path="skin->project", debug=debug) + project_overrides_wrapped = {"tokens": manifest.get("overrides", {}).get("tokens", {})} + final_context = self._deep_merge( + context, project_overrides_wrapped, path="skin->project", debug=debug + ) # Inject Metadata final_context["_meta"] = { "project_id": manifest["project"]["id"], "compiled_at": datetime.now(timezone.utc).isoformat(), "debug_enabled": debug, - "compiler_config": manifest.get("compiler", {}) + "compiler_config": manifest.get("compiler", {}), } if debug: @@ -138,19 +139,28 @@ class ContextCompiler: return data def _load_json(self, path: str) -> Dict[str, Any]: - with open(path, 'r') as f: + with open(path, "r") as f: return json.load(f) - def _deep_merge(self, base: Dict, override: Dict, path: str = "", debug: bool = False, provenance: List[Dict] = None) -> Dict: + def _deep_merge( + self, + base: Dict, + override: Dict, + path: str = "", + debug: bool = False, + provenance: List[Dict] = None, + ) -> Dict: """ - Deep merge dictionaries. Replaces arrays. + Deep merge dictionaries. + + Replaces arrays. Populates provenance list if debug is True (thread-safe). """ # Thread-safe: use method parameter instead of instance variable if provenance is None and debug: provenance = [] # Store reference on first call for later retrieval - if not hasattr(self, 'provenance_log'): + if not hasattr(self, "provenance_log"): self.provenance_log = provenance result = copy.deepcopy(base) @@ -158,16 +168,20 @@ class ContextCompiler: for key, value in override.items(): if isinstance(value, dict) and key in result and isinstance(result[key], dict): # Recursive merge - pass provenance down - result[key] = self._deep_merge(result[key], value, path=f"{path}.{key}", debug=debug, provenance=provenance) + result[key] = self._deep_merge( + result[key], value, path=f"{path}.{key}", debug=debug, provenance=provenance + ) else: # Direct replacement (Primitive or Array) if debug and provenance is not None: - provenance.append({ - "key": key, - "action": "override", - "layer": path, - "value_type": type(value).__name__ - }) + provenance.append( + { + "key": key, + "action": "override", + "layer": path, + "value_type": type(value).__name__, + } + ) result[key] = copy.deepcopy(value) return result diff --git a/dss-claude-plugin/core/config.py b/dss-claude-plugin/core/config.py index d0aefaf..8a53019 100644 --- a/dss-claude-plugin/core/config.py +++ b/dss-claude-plugin/core/config.py @@ -7,17 +7,15 @@ Supports local/remote mode detection, persistent configuration storage, and environment variable overrides. """ -import os import json -import uuid -import asyncio import logging +import os +import uuid from enum import Enum from pathlib import Path -from typing import Optional, Union, Any import aiohttp -from pydantic import BaseModel, Field, HttpUrl, ValidationError +from pydantic import BaseModel, Field, ValidationError # Configure module-level logger logger = logging.getLogger(__name__) @@ -30,6 +28,7 @@ DEFAULT_LOCAL_URL = "http://localhost:6006" class DSSMode(str, Enum): """Operation modes for the DSS plugin.""" + LOCAL = "local" REMOTE = "remote" AUTO = "auto" @@ -45,10 +44,13 @@ class DSSConfig(BaseModel): local_url (str): URL for the local DSS API (usually localhost). session_id (str): Unique identifier for this client instance. """ + mode: DSSMode = Field(default=DSSMode.AUTO, description="Operation mode preference") remote_url: str = Field(default=DEFAULT_REMOTE_URL, description="Remote API endpoint") local_url: str = Field(default=DEFAULT_LOCAL_URL, description="Local API endpoint") - session_id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Persistent session ID") + session_id: str = Field( + default_factory=lambda: str(uuid.uuid4()), description="Persistent session ID" + ) class Config: validate_assignment = True @@ -58,6 +60,7 @@ class DSSConfig(BaseModel): def load(cls) -> "DSSConfig": """ Load configuration from ~/.dss/config.json. + Returns a default instance if the file does not exist or is invalid. """ if not CONFIG_FILE.exists(): @@ -79,6 +82,7 @@ class DSSConfig(BaseModel): def save(self) -> None: """ Save the current configuration to ~/.dss/config.json. + Creates the directory if it does not exist. """ try: @@ -153,9 +157,7 @@ class DSSConfig(BaseModel): return False def get_api_url(self, active_mode: DSSMode) -> str: - """ - Helper to get the correct API URL for the determined mode. - """ + """Helper to get the correct API URL for the determined mode.""" if active_mode == DSSMode.LOCAL: return self.local_url return self.remote_url diff --git a/dss-claude-plugin/core/context.py b/dss-claude-plugin/core/context.py index 9843748..8819b43 100644 --- a/dss-claude-plugin/core/context.py +++ b/dss-claude-plugin/core/context.py @@ -8,7 +8,7 @@ Handles configuration loading, mode detection, and strategy instantiation. import asyncio import logging -from typing import Optional, Dict, Any +from typing import Any, Dict, Optional from .config import DSSConfig, DSSMode @@ -26,12 +26,15 @@ class DSSContext: Handles configuration loading, mode detection (Local/Remote), and strategy instantiation. """ - _instance: Optional['DSSContext'] = None + + _instance: Optional["DSSContext"] = None _lock: asyncio.Lock = asyncio.Lock() def __init__(self) -> None: """ - Private initializer. Use get_instance() instead. + Private initializer. + + Use get_instance() instead. """ if DSSContext._instance is not None: raise RuntimeError("DSSContext is a singleton. Use get_instance() to access it.") @@ -43,9 +46,10 @@ class DSSContext: self.session_id: Optional[str] = None @classmethod - async def get_instance(cls) -> 'DSSContext': + async def get_instance(cls) -> "DSSContext": """ Async factory method to get the singleton instance. + Ensures config is loaded and mode is detected before returning. """ if not cls._instance: @@ -61,13 +65,16 @@ class DSSContext: @classmethod def reset(cls) -> None: """ - Resets the singleton instance. Useful for testing. + Resets the singleton instance. + + Useful for testing. """ cls._instance = None async def _initialize(self) -> None: """ Internal initialization logic: + 1. Load Config 2. Detect Mode 3. Cache Capabilities @@ -80,7 +87,9 @@ class DSSContext: # 2. Detect Mode (Async check) self.active_mode = await self.config.get_active_mode() - logger.info(f"DSSContext initialized. Mode: {self.active_mode.value}, Session: {self.session_id}") + logger.info( + f"DSSContext initialized. Mode: {self.active_mode.value}, Session: {self.session_id}" + ) # 3. Cache Capabilities self._cache_capabilities() @@ -92,15 +101,13 @@ class DSSContext: self._capabilities = {"limited": True} def _cache_capabilities(self) -> None: - """ - Determines what the plugin can do based on the active mode. - """ + """Determines what the plugin can do based on the active mode.""" # Base capabilities caps = { "can_read_files": False, "can_execute_browser": False, "can_screenshot": False, - "can_connect_remote": True + "can_connect_remote": True, } if self.active_mode == DSSMode.LOCAL: @@ -111,8 +118,10 @@ class DSSContext: elif self.active_mode == DSSMode.REMOTE: # Remote mode relies on API capabilities # Depending on remote configuration, these might differ - caps["can_execute_browser"] = False # Typically restricted in pure remote unless via API - caps["can_read_files"] = False # Security restriction + caps[ + "can_execute_browser" + ] = False # Typically restricted in pure remote unless via API + caps["can_read_files"] = False # Security restriction self._capabilities = caps @@ -151,18 +160,22 @@ class DSSContext: # Will be implemented in Phase 2 & 3 if self.active_mode == DSSMode.LOCAL: from ..strategies.local.browser import LocalBrowserStrategy + strategy_instance = LocalBrowserStrategy(self) else: from ..strategies.remote.browser import RemoteBrowserStrategy + strategy_instance = RemoteBrowserStrategy(self) elif strategy_type == "filesystem": # Will be implemented in Phase 2 if self.active_mode == DSSMode.LOCAL: from ..strategies.local.filesystem import LocalFilesystemStrategy + strategy_instance = LocalFilesystemStrategy(self) else: from ..strategies.remote.filesystem import RemoteFilesystemStrategy + strategy_instance = RemoteFilesystemStrategy(self) elif strategy_type == "screenshot": diff --git a/dss-claude-plugin/core/mcp_extensions.py b/dss-claude-plugin/core/mcp_extensions.py index 5b36eca..22225c0 100644 --- a/dss-claude-plugin/core/mcp_extensions.py +++ b/dss-claude-plugin/core/mcp_extensions.py @@ -1,13 +1,15 @@ """ -MCP Extensions for Context Awareness +MCP Extensions for Context Awareness. + Implements the Factory Pattern to wrap existing tools with context and defines 5 new tools for the Context Compiler. """ -from typing import Any, Dict, List, Callable import functools import json import os +from typing import Callable + from .compiler import ContextCompiler # Singleton compiler instance @@ -15,19 +17,22 @@ COMPILER = ContextCompiler(skins_dir=os.path.join(os.path.dirname(__file__), "sk # --- FACTORY PATTERN: Context Wrapper --- + def with_context(default_manifest_path: str = None): """ Decorator that injects the compiled context into the tool's arguments. + Use this to upgrade existing 'token extractor' tools to be 'context aware'. The manifest path is extracted from kwargs['manifest_path'] if present, otherwise falls back to the default_manifest_path provided at decoration time. """ + def decorator(func: Callable): @functools.wraps(func) def wrapper(*args, **kwargs): # 1. Get manifest path (runtime kwarg or decorator default) - manifest_path = kwargs.get('manifest_path', default_manifest_path) + manifest_path = kwargs.get("manifest_path", default_manifest_path) if not manifest_path: raise ValueError("No manifest_path provided to context-aware tool") @@ -35,33 +40,39 @@ def with_context(default_manifest_path: str = None): context = COMPILER.compile(manifest_path) # 3. Inject into kwargs - kwargs['dss_context'] = context + kwargs["dss_context"] = context # 4. Execute Tool return func(*args, **kwargs) + return wrapper + return decorator # --- 5 NEW MCP TOOLS --- + def get_active_context(manifest_path: str, debug: bool = False, force_refresh: bool = False) -> str: """ [Tool 1] Returns the fully resolved JSON context for the project. + Set debug=True to see provenance (which layer defined which token). Set force_refresh=True to bypass cache (for long-running servers). """ context = COMPILER.compile(manifest_path, debug=debug, force_refresh=force_refresh) return json.dumps(context, indent=2) + def resolve_token(manifest_path: str, token_path: str, force_refresh: bool = False) -> str: """ - [Tool 2] Resolves a specific token value (e.g. 'colors.primary') + [Tool 2] Resolves a specific token value (e.g. 'colors.primary'). + through the cascade. Set force_refresh=True to bypass cache (for long-running servers). """ context = COMPILER.compile(manifest_path, force_refresh=force_refresh) - keys = token_path.split('.') + keys = token_path.split(".") current = context.get("tokens", {}) for k in keys: @@ -72,10 +83,9 @@ def resolve_token(manifest_path: str, token_path: str, force_refresh: bool = Fal return str(current) + def validate_manifest(manifest_path: str) -> str: - """ - [Tool 3] Validates the ds.config.json against the schema. - """ + """[Tool 3] Validates the ds.config.json against the schema.""" # In a full implementation, we would use 'jsonschema' library here. # For now, we perform a basic structural check via the Compiler's loader. try: @@ -84,10 +94,9 @@ def validate_manifest(manifest_path: str) -> str: except Exception as e: return f"Invalid: {str(e)}" + def list_skins() -> str: - """ - [Tool 4] Lists all available skins in the registry. - """ + """[Tool 4] Lists all available skins in the registry.""" skins_path = COMPILER.skins_dir if not skins_path.exists(): return "No skins directory found." @@ -95,18 +104,18 @@ def list_skins() -> str: skins = [f.stem for f in skins_path.glob("*.json")] return json.dumps(skins) + def get_compiler_status() -> str: - """ - [Tool 5] Returns the health and configuration of the Context Compiler. - """ + """[Tool 5] Returns the health and configuration of the Context Compiler.""" status = { "status": "active", "skins_directory": str(COMPILER.skins_dir), "cached_skins": list(COMPILER.cache.keys()), - "safe_boot_ready": True + "safe_boot_ready": True, } return json.dumps(status, indent=2) + # Instructions for Main Server File: # 1. Import these tools # 2. Register them with the MCP server instance diff --git a/dss-claude-plugin/core/mcp_integration.py b/dss-claude-plugin/core/mcp_integration.py index 4975d44..cda05fc 100644 --- a/dss-claude-plugin/core/mcp_integration.py +++ b/dss-claude-plugin/core/mcp_integration.py @@ -1,23 +1,21 @@ """ -MCP Integration Layer for DSS Context Compiler +MCP Integration Layer for DSS Context Compiler. + Provides MCP-compliant tool wrappers for the 5 new context tools. """ -from typing import Dict, Any import json -from . import ( - get_active_context, - resolve_token, - validate_manifest, - list_skins, - get_compiler_status -) + +from . import get_active_context, get_compiler_status, list_skins, resolve_token, validate_manifest # MCP Tool Definitions -def mcp_get_resolved_context(manifest_path: str, debug: bool = False, force_refresh: bool = False) -> str: + +def mcp_get_resolved_context( + manifest_path: str, debug: bool = False, force_refresh: bool = False +) -> str: """ - MCP Tool: Get Active Context + MCP Tool: Get Active Context. Returns the fully resolved JSON context for a project. Set debug=True to see provenance (which layer defined which token). @@ -39,7 +37,7 @@ def mcp_get_resolved_context(manifest_path: str, debug: bool = False, force_refr def mcp_resolve_token(manifest_path: str, token_path: str, force_refresh: bool = False) -> str: """ - MCP Tool: Resolve Token + MCP Tool: Resolve Token. Resolves a specific token value (e.g. 'colors.primary') through the cascade. Set force_refresh=True to bypass cache (for long-running servers). @@ -60,7 +58,7 @@ def mcp_resolve_token(manifest_path: str, token_path: str, force_refresh: bool = def mcp_validate_manifest(manifest_path: str) -> str: """ - MCP Tool: Validate Manifest + MCP Tool: Validate Manifest. Validates the ds.config.json against the schema. @@ -78,7 +76,7 @@ def mcp_validate_manifest(manifest_path: str) -> str: def mcp_list_skins() -> str: """ - MCP Tool: List Skins + MCP Tool: List Skins. Lists all available skins in the registry. @@ -93,7 +91,7 @@ def mcp_list_skins() -> str: def mcp_get_compiler_status() -> str: """ - MCP Tool: Get Compiler Status + MCP Tool: Get Compiler Status. Returns the health and configuration of the Context Compiler. @@ -117,15 +115,15 @@ MCP_TOOLS = { "manifest_path": { "type": "string", "description": "Path to ds.config.json", - "required": True + "required": True, }, "debug": { "type": "boolean", "description": "Enable debug provenance tracking", "required": False, - "default": False - } - } + "default": False, + }, + }, }, "dss_resolve_token": { "function": mcp_resolve_token, @@ -134,14 +132,14 @@ MCP_TOOLS = { "manifest_path": { "type": "string", "description": "Path to ds.config.json", - "required": True + "required": True, }, "token_path": { "type": "string", "description": "Dot-notation path to token (e.g. 'colors.primary')", - "required": True - } - } + "required": True, + }, + }, }, "dss_validate_manifest": { "function": mcp_validate_manifest, @@ -150,18 +148,18 @@ MCP_TOOLS = { "manifest_path": { "type": "string", "description": "Path to ds.config.json", - "required": True + "required": True, } - } + }, }, "dss_list_skins": { "function": mcp_list_skins, "description": "List all available design system skins", - "parameters": {} + "parameters": {}, }, "dss_get_compiler_status": { "function": mcp_get_compiler_status, "description": "Get Context Compiler health and configuration", - "parameters": {} - } + "parameters": {}, + }, } diff --git a/dss-claude-plugin/core/runtime.py b/dss-claude-plugin/core/runtime.py index 0471254..31f5548 100644 --- a/dss-claude-plugin/core/runtime.py +++ b/dss-claude-plugin/core/runtime.py @@ -1,5 +1,5 @@ """ -DSS Runtime - Dependency Injection & Boundary Enforcement +DSS Runtime - Dependency Injection & Boundary Enforcement. This module provides a bounded runtime environment for DSS MCP tools. All external API access (Figma, Browser, HTTP) MUST go through this runtime. @@ -16,20 +16,24 @@ Usage: browser = runtime.get_browser() # Sandboxed """ -import logging import json -from pathlib import Path -from typing import Optional, Dict, Any, List +import logging from datetime import datetime +from pathlib import Path +from typing import Any, Dict, Optional + import yaml # Setup logging logger = logging.getLogger("dss.runtime") + class BoundaryViolationError(Exception): - """Raised when an operation violates DSS boundaries""" + """Raised when an operation violates DSS boundaries.""" + pass + class DSSRuntime: """ Bounded runtime environment for DSS operations. @@ -52,7 +56,11 @@ class DSSRuntime: self.config = self._load_config() self.enforcement_mode = self.config.get("enforcement", {}).get("mode", "strict") self.log_violations = self.config.get("enforcement", {}).get("log_violations", True) - self.violation_log_path = Path(self.config.get("enforcement", {}).get("violation_log", ".dss/logs/boundary-violations.jsonl")) + self.violation_log_path = Path( + self.config.get("enforcement", {}).get( + "violation_log", ".dss/logs/boundary-violations.jsonl" + ) + ) # Client caches (lazy initialization) self._figma_client = None @@ -62,7 +70,7 @@ class DSSRuntime: logger.info(f"DSSRuntime initialized with enforcement mode: {self.enforcement_mode}") def _load_config(self) -> Dict[str, Any]: - """Load boundary configuration from YAML""" + """Load boundary configuration from YAML.""" if not self.config_path.exists(): logger.warning(f"Boundary config not found: {self.config_path}, using defaults") return self._default_config() @@ -75,7 +83,7 @@ class DSSRuntime: return self._default_config() def _default_config(self) -> Dict[str, Any]: - """Default boundary configuration (strict)""" + """Default boundary configuration (strict).""" return { "version": "1.0", "blocked_external_apis": ["api.figma.com"], @@ -83,12 +91,12 @@ class DSSRuntime: "enforcement": { "mode": "strict", "log_violations": True, - "violation_log": ".dss/logs/boundary-violations.jsonl" - } + "violation_log": ".dss/logs/boundary-violations.jsonl", + }, } def _log_violation(self, operation: str, details: Dict[str, Any]): - """Log boundary violation to audit trail""" + """Log boundary violation to audit trail.""" if not self.log_violations: return @@ -99,7 +107,7 @@ class DSSRuntime: "type": "boundary_violation", "operation": operation, "enforcement_mode": self.enforcement_mode, - "details": details + "details": details, } with open(self.violation_log_path, "a") as f: @@ -108,7 +116,7 @@ class DSSRuntime: logger.warning(f"Boundary violation: {operation} - {details}") def _log_access(self, operation: str, allowed: bool, details: Dict[str, Any]): - """Log successful access for audit trail""" + """Log successful access for audit trail.""" access_log_path = Path(".dss/logs/runtime-access.jsonl") access_log_path.parent.mkdir(parents=True, exist_ok=True) @@ -117,7 +125,7 @@ class DSSRuntime: "type": "runtime_access", "operation": operation, "allowed": allowed, - "details": details + "details": details, } with open(access_log_path, "a") as f: @@ -139,11 +147,7 @@ class DSSRuntime: # Check if operation requires going through DSS tools for category, tools in required_tools.items(): if operation in category: - details = { - "operation": operation, - "context": context, - "required_tools": tools - } + details = {"operation": operation, "context": context, "required_tools": tools} self._log_violation(operation, details) @@ -173,8 +177,8 @@ class DSSRuntime: self._figma_client = SafeFigmaClient( token=token, - allow_write=False, # Read-only by default - runtime=self + allow_write=False, + runtime=self, # Read-only by default ) logger.info("Figma client initialized (read-only mode)") @@ -195,6 +199,7 @@ class DSSRuntime: if strategy == "local": try: from strategies.local.browser import LocalBrowserStrategy + self._browser_strategy = LocalBrowserStrategy(runtime=self) logger.info("Local browser strategy initialized") except ImportError: @@ -204,6 +209,7 @@ class DSSRuntime: elif strategy == "remote": try: from strategies.remote.browser import RemoteBrowserStrategy + self._browser_strategy = RemoteBrowserStrategy(runtime=self) logger.info("Remote browser strategy initialized") except ImportError: @@ -224,8 +230,7 @@ class DSSRuntime: from core.safe_http_client import SafeHTTPClient self._http_client = SafeHTTPClient( - blocked_domains=self.config.get("blocked_external_apis", []), - runtime=self + blocked_domains=self.config.get("blocked_external_apis", []), runtime=self ) logger.info("HTTP client initialized with URL validation") @@ -245,10 +250,7 @@ class DSSRuntime: blocked = self.config.get("blocked_imports", []) if module_name in blocked: - details = { - "module": module_name, - "blocked_imports": blocked - } + details = {"module": module_name, "blocked_imports": blocked} self._log_violation(f"direct_import:{module_name}", details) @@ -292,14 +294,16 @@ class DSSRuntime: "browser": self._browser_strategy is not None, "http": self._http_client is not None, }, - "config_version": self.config.get("version", "unknown") + "config_version": self.config.get("version", "unknown"), } + # Global runtime instance (singleton pattern) _runtime_instance: Optional[DSSRuntime] = None + def get_runtime() -> DSSRuntime: - """Get the global DSSRuntime instance (singleton)""" + """Get the global DSSRuntime instance (singleton).""" global _runtime_instance if _runtime_instance is None: diff --git a/dss-claude-plugin/core/structured_logger.py b/dss-claude-plugin/core/structured_logger.py index dc95837..4c4f100 100644 --- a/dss-claude-plugin/core/structured_logger.py +++ b/dss-claude-plugin/core/structured_logger.py @@ -1,5 +1,5 @@ """ -DSS Structured Logger - JSON-based logging for AI-consumable audit trails +DSS Structured Logger - JSON-based logging for AI-consumable audit trails. Provides structured, machine-readable logging in JSONL format (one JSON object per line). All DSS operations are logged with consistent fields for analysis, debugging, and compliance. @@ -27,11 +27,11 @@ import json import logging import os import sys +import threading +from contextlib import contextmanager from datetime import datetime, timezone from pathlib import Path from typing import Any, Dict, Optional -from contextlib import contextmanager -import threading # Thread-local storage for context _context = threading.local() @@ -51,7 +51,7 @@ class DSSJSONFormatter(logging.Formatter): """ def format(self, record: logging.LogRecord) -> str: - """Format log record as single-line JSON""" + """Format log record as single-line JSON.""" # Build base log entry log_entry = { @@ -100,8 +100,10 @@ class DSSLogger(logging.Logger): as keyword arguments for structured logging. """ - def _log_with_extra(self, level: int, msg: str, extra: Optional[Dict[str, Any]] = None, **kwargs): - """Internal method to log with extra structured data""" + def _log_with_extra( + self, level: int, msg: str, extra: Optional[Dict[str, Any]] = None, **kwargs + ): + """Internal method to log with extra structured data.""" if extra: # Store extra data in a custom attribute extra_record = {"extra_data": extra} @@ -110,23 +112,23 @@ class DSSLogger(logging.Logger): super()._log(level, msg, (), **kwargs) def debug(self, msg: str, extra: Optional[Dict[str, Any]] = None, **kwargs): - """Log DEBUG message with optional extra data""" + """Log DEBUG message with optional extra data.""" self._log_with_extra(logging.DEBUG, msg, extra, **kwargs) def info(self, msg: str, extra: Optional[Dict[str, Any]] = None, **kwargs): - """Log INFO message with optional extra data""" + """Log INFO message with optional extra data.""" self._log_with_extra(logging.INFO, msg, extra, **kwargs) def warning(self, msg: str, extra: Optional[Dict[str, Any]] = None, **kwargs): - """Log WARNING message with optional extra data""" + """Log WARNING message with optional extra data.""" self._log_with_extra(logging.WARNING, msg, extra, **kwargs) def error(self, msg: str, extra: Optional[Dict[str, Any]] = None, **kwargs): - """Log ERROR message with optional extra data""" + """Log ERROR message with optional extra data.""" self._log_with_extra(logging.ERROR, msg, extra, **kwargs) def critical(self, msg: str, extra: Optional[Dict[str, Any]] = None, **kwargs): - """Log CRITICAL message with optional extra data""" + """Log CRITICAL message with optional extra data.""" self._log_with_extra(logging.CRITICAL, msg, extra, **kwargs) @@ -182,7 +184,9 @@ def get_logger(name: str, log_file: Optional[str] = None) -> DSSLogger: @contextmanager -def LogContext(session_id: Optional[str] = None, tool: Optional[str] = None, operation: Optional[str] = None): +def LogContext( + session_id: Optional[str] = None, tool: Optional[str] = None, operation: Optional[str] = None +): """ Context manager for adding structured context to log entries. @@ -259,12 +263,15 @@ class PerformanceLogger: self.end_time = None def start(self): - """Mark operation start time""" + """Mark operation start time.""" self.start_time = datetime.now(timezone.utc) - self.logger.debug(f"Started: {self.operation}", extra={ - "operation": self.operation, - "start_time": self.start_time.isoformat(), - }) + self.logger.debug( + f"Started: {self.operation}", + extra={ + "operation": self.operation, + "start_time": self.start_time.isoformat(), + }, + ) def end(self, extra: Optional[Dict[str, Any]] = None): """ @@ -276,7 +283,9 @@ class PerformanceLogger: self.end_time = datetime.now(timezone.utc) if self.start_time is None: - self.logger.warning(f"Performance logger end() called without start() for: {self.operation}") + self.logger.warning( + f"Performance logger end() called without start() for: {self.operation}" + ) return duration_ms = (self.end_time - self.start_time).total_seconds() * 1000 @@ -294,7 +303,9 @@ class PerformanceLogger: self.logger.info(f"Completed: {self.operation}", extra=perf_data) -def configure_log_rotation(log_dir: Optional[Path] = None, max_bytes: int = 10 * 1024 * 1024, backup_count: int = 5): +def configure_log_rotation( + log_dir: Optional[Path] = None, max_bytes: int = 10 * 1024 * 1024, backup_count: int = 5 +): """ Configure log rotation for DSS log files. @@ -325,19 +336,19 @@ def configure_log_rotation(log_dir: Optional[Path] = None, max_bytes: int = 10 * # Add rotating file handler rotating_handler = RotatingFileHandler( - str(log_file), - maxBytes=max_bytes, - backupCount=backup_count, - encoding="utf-8" + str(log_file), maxBytes=max_bytes, backupCount=backup_count, encoding="utf-8" ) rotating_handler.setFormatter(DSSJSONFormatter()) logger.addHandler(rotating_handler) - logger.info("Log rotation configured", extra={ - "max_bytes": max_bytes, - "backup_count": backup_count, - "log_file": str(log_file), - }) + logger.info( + "Log rotation configured", + extra={ + "max_bytes": max_bytes, + "backup_count": backup_count, + "log_file": str(log_file), + }, + ) # Example usage (can be removed in production) @@ -356,6 +367,7 @@ if __name__ == "__main__": perf.start() # Simulate work import time + time.sleep(0.1) perf.end(extra={"tokens_found": 100}) diff --git a/dss-claude-plugin/hooks/.state/.git-backup.lock b/dss-claude-plugin/hooks/.state/.git-backup.lock index 2195d73..179bfb9 100644 --- a/dss-claude-plugin/hooks/.state/.git-backup.lock +++ b/dss-claude-plugin/hooks/.state/.git-backup.lock @@ -1 +1 @@ -1765445463969 \ No newline at end of file +1765446683593 diff --git a/dss-claude-plugin/hooks/dss-hooks-config.json b/dss-claude-plugin/hooks/dss-hooks-config.json index 57f7814..4d85002 100644 --- a/dss-claude-plugin/hooks/dss-hooks-config.json +++ b/dss-claude-plugin/hooks/dss-hooks-config.json @@ -1,27 +1,27 @@ { "description": "DSS Hooks Configuration - Customize hook behavior", "version": "1.0.0", - + "security_check": { "enabled": true, "block_on_critical": false, "warn_only": true, "ignored_patterns": [] }, - + "token_validator": { "enabled": true, "strict_mode": false, "warn_only": true, "categories": ["color", "spacing", "typography", "border", "effects", "layout"] }, - + "component_checker": { "enabled": true, "categories": ["accessibility", "react", "typescript", "structure"], "min_severity": "low" }, - + "complexity_monitor": { "enabled": true, "max_function_lines": 50, @@ -30,7 +30,7 @@ "max_nesting_depth": 4, "warn_only": true }, - + "storybook_reminder": { "enabled": true, "component_patterns": ["**/components/**/*.tsx", "**/ui/**/*.tsx"], @@ -38,7 +38,7 @@ "remind_on_new": true, "remind_on_props_change": true }, - + "session_summary": { "enabled": true, "output_file": ".dss-session-summary.md", @@ -46,7 +46,7 @@ "include_file_list": true, "max_diff_lines": 100 }, - + "git_backup": { "enabled": true, "require_git_repo": true, diff --git a/dss-claude-plugin/hooks/scripts/complexity-monitor.js b/dss-claude-plugin/hooks/scripts/complexity-monitor.js index 81bb3cb..64622c1 100755 --- a/dss-claude-plugin/hooks/scripts/complexity-monitor.js +++ b/dss-claude-plugin/hooks/scripts/complexity-monitor.js @@ -55,7 +55,7 @@ function countProps(content) { function countNestingDepth(content) { let maxDepth = 0; let currentDepth = 0; - + for (const char of content) { if (char === '{' || char === '(') { currentDepth++; @@ -64,7 +64,7 @@ function countNestingDepth(content) { currentDepth = Math.max(0, currentDepth - 1); } } - + return maxDepth; } @@ -74,7 +74,7 @@ function countFunctions(content) { /const\s+\w+\s*=\s*(?:async\s*)?\([^)]*\)\s*=>/g, /const\s+\w+\s*=\s*(?:async\s*)?function/g ]; - + let count = 0; for (const pattern of patterns) { const matches = content.match(pattern); @@ -87,17 +87,17 @@ function analyzeComplexity(content, filePath, config) { const issues = []; const monitorConfig = config.complexity_monitor || {}; const ext = path.extname(filePath).toLowerCase(); - + // Only analyze JS/TS files if (!['.js', '.jsx', '.ts', '.tsx'].includes(ext)) { return issues; } - + const lines = countLines(content); const props = countProps(content); const nesting = countNestingDepth(content); const functions = countFunctions(content); - + // Check component size (for tsx/jsx files) if (['.tsx', '.jsx'].includes(ext)) { if (lines > monitorConfig.max_component_lines) { @@ -108,7 +108,7 @@ function analyzeComplexity(content, filePath, config) { suggestion: 'Consider breaking into smaller components' }); } - + if (props > monitorConfig.max_props) { issues.push({ type: 'prop_count', @@ -118,7 +118,7 @@ function analyzeComplexity(content, filePath, config) { }); } } - + // Check nesting depth if (nesting > monitorConfig.max_nesting_depth) { issues.push({ @@ -128,7 +128,7 @@ function analyzeComplexity(content, filePath, config) { suggestion: 'Extract nested logic into separate functions' }); } - + // Check function count (indicator of file doing too much) if (functions > 10) { issues.push({ @@ -138,38 +138,38 @@ function analyzeComplexity(content, filePath, config) { suggestion: 'Consider splitting into multiple modules' }); } - + return issues; } function formatOutput(issues, filePath) { if (issues.length === 0) return ''; - + const severityIcons = { high: '[HIGH]', medium: '[MED]', low: '[LOW]' }; - + const lines = [`\n=== DSS Complexity Monitor: ${filePath} ===\n`]; - + for (const issue of issues) { const icon = severityIcons[issue.severity] || '[?]'; lines.push(`${icon} ${issue.message}`); lines.push(` Suggestion: ${issue.suggestion}\n`); } - + lines.push('='.repeat(50)); return lines.join('\n'); } async function main() { const config = loadConfig(); - + if (!config.complexity_monitor?.enabled) { process.exit(0); } - + // Read input from stdin let inputData; try { @@ -181,34 +181,34 @@ async function main() { } catch (e) { process.exit(0); } - + const toolName = inputData.tool_name || ''; const toolInput = inputData.tool_input || {}; - + if (!['Edit', 'Write'].includes(toolName)) { process.exit(0); } - + const filePath = toolInput.file_path || ''; let content = ''; - + if (toolName === 'Write') { content = toolInput.content || ''; } else if (toolName === 'Edit') { content = toolInput.new_string || ''; } - + if (!content || !filePath) { process.exit(0); } - + const issues = analyzeComplexity(content, filePath, config); - + if (issues.length > 0) { const output = formatOutput(issues, filePath); console.error(output); } - + process.exit(0); } diff --git a/dss-claude-plugin/hooks/scripts/component-checker.py b/dss-claude-plugin/hooks/scripts/component-checker.py index 873674f..23d875d 100755 --- a/dss-claude-plugin/hooks/scripts/component-checker.py +++ b/dss-claude-plugin/hooks/scripts/component-checker.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 """ -DSS Component Checker Hook +DSS Component Checker Hook. + Validates React components for best practices and accessibility. Written from scratch for DSS. """ @@ -19,7 +20,7 @@ COMPONENT_PATTERNS = [ "category": "accessibility", "severity": "high", "message": "Missing alt attribute on . Add alt text for accessibility.", - "file_types": [".jsx", ".tsx"] + "file_types": [".jsx", ".tsx"], }, { "id": "a11y-button-type", @@ -27,7 +28,7 @@ COMPONENT_PATTERNS = [ "category": "accessibility", "severity": "medium", "message": "Button missing type attribute. Add type='button' or type='submit'.", - "file_types": [".jsx", ".tsx"] + "file_types": [".jsx", ".tsx"], }, { "id": "a11y-anchor-href", @@ -35,7 +36,7 @@ COMPONENT_PATTERNS = [ "category": "accessibility", "severity": "high", "message": "Anchor tag missing href. Use button for actions without navigation.", - "file_types": [".jsx", ".tsx"] + "file_types": [".jsx", ".tsx"], }, { "id": "a11y-click-handler", @@ -43,7 +44,7 @@ COMPONENT_PATTERNS = [ "category": "accessibility", "severity": "medium", "message": "Click handler on non-interactive element. Use ', + r"]*>\s*<(?:svg|Icon|img)[^>]*/?>\s*", content, - re.IGNORECASE + re.IGNORECASE, ) if icon_only_buttons: - a11y_issues.append({ - 'type': 'icon-button-no-label', - 'file': rel_path, - }) + a11y_issues.append( + { + "type": "icon-button-no-label", + "file": rel_path, + } + ) # Check for click handlers on non-interactive elements - div_onclick = re.findall(r']+onClick', content) + div_onclick = re.findall(r"]+onClick", content) if div_onclick: - a11y_issues.append({ - 'type': 'div-click-handler', - 'file': rel_path, - 'count': len(div_onclick), - }) + a11y_issues.append( + { + "type": "div-click-handler", + "file": rel_path, + "count": len(div_onclick), + } + ) except Exception: continue # Group issues by type if a11y_issues: - img_issues = [i for i in a11y_issues if i['type'] == 'img-no-alt'] + img_issues = [i for i in a11y_issues if i["type"] == "img-no-alt"] if img_issues: - wins.append(QuickWin( - type=QuickWinType.ACCESSIBILITY, - priority=QuickWinPriority.HIGH, - title=f"Found {len(img_issues)} images without alt text", - description="Images should have alt attributes for screen readers. Empty alt='' is acceptable for decorative images.", - affected_files=list(set(i['file'] for i in img_issues))[:10], - estimated_impact="Improve accessibility for screen reader users", - fix_suggestion="Add descriptive alt text to images or alt='' for decorative images.", - auto_fixable=False, - )) + wins.append( + QuickWin( + type=QuickWinType.ACCESSIBILITY, + priority=QuickWinPriority.HIGH, + title=f"Found {len(img_issues)} images without alt text", + description="Images should have alt attributes for screen readers. Empty alt='' is acceptable for decorative images.", + affected_files=list(set(i["file"] for i in img_issues))[:10], + estimated_impact="Improve accessibility for screen reader users", + fix_suggestion="Add descriptive alt text to images or alt='' for decorative images.", + auto_fixable=False, + ) + ) - div_issues = [i for i in a11y_issues if i['type'] == 'div-click-handler'] + div_issues = [i for i in a11y_issues if i["type"] == "div-click-handler"] if div_issues: - wins.append(QuickWin( - type=QuickWinType.ACCESSIBILITY, - priority=QuickWinPriority.MEDIUM, - title=f"Found click handlers on div elements", - description="Using onClick on div elements makes them inaccessible to keyboard users. Use button or add proper ARIA attributes.", - affected_files=list(set(i['file'] for i in div_issues))[:10], - estimated_impact="Improve keyboard navigation accessibility", - fix_suggestion="Replace
with