@codyswann/lisa 2.178.2 → 2.178.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/dist/codex/scripts/install-pkgs.sh +27 -1
  2. package/dist/opencode/plugin-templates/lisa-session-bootstrap.ts +18 -5
  3. package/package.json +1 -1
  4. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  5. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  6. package/plugins/lisa/hooks/install-pkgs.sh +26 -1
  7. package/plugins/lisa-agy/plugin.json +1 -1
  8. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  9. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  10. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  11. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  12. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  13. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  14. package/plugins/lisa-copilot/hooks/install-pkgs.sh +26 -1
  15. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  16. package/plugins/lisa-cursor/hooks/install-pkgs.sh +26 -1
  17. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  18. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  19. package/plugins/lisa-expo/skills/atomic-design-gluestack/SKILL.md +8 -0
  20. package/plugins/lisa-expo/skills/atomic-design-gluestack/scripts/test_validate_atomic_structure.py +240 -0
  21. package/plugins/lisa-expo/skills/atomic-design-gluestack/scripts/validate_atomic_structure.py +109 -0
  22. package/plugins/lisa-expo/skills/gluestack-nativewind/SKILL.md +8 -0
  23. package/plugins/lisa-expo/skills/gluestack-nativewind/scripts/test_validate_styling.py +240 -0
  24. package/plugins/lisa-expo/skills/gluestack-nativewind/scripts/validate_styling.py +109 -0
  25. package/plugins/lisa-expo-agy/plugin.json +1 -1
  26. package/plugins/lisa-expo-agy/skills/atomic-design-gluestack/SKILL.md +8 -0
  27. package/plugins/lisa-expo-agy/skills/atomic-design-gluestack/scripts/test_validate_atomic_structure.py +240 -0
  28. package/plugins/lisa-expo-agy/skills/atomic-design-gluestack/scripts/validate_atomic_structure.py +109 -0
  29. package/plugins/lisa-expo-agy/skills/gluestack-nativewind/SKILL.md +8 -0
  30. package/plugins/lisa-expo-agy/skills/gluestack-nativewind/scripts/test_validate_styling.py +240 -0
  31. package/plugins/lisa-expo-agy/skills/gluestack-nativewind/scripts/validate_styling.py +109 -0
  32. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  33. package/plugins/lisa-expo-copilot/skills/atomic-design-gluestack/SKILL.md +8 -0
  34. package/plugins/lisa-expo-copilot/skills/atomic-design-gluestack/scripts/test_validate_atomic_structure.py +240 -0
  35. package/plugins/lisa-expo-copilot/skills/atomic-design-gluestack/scripts/validate_atomic_structure.py +109 -0
  36. package/plugins/lisa-expo-copilot/skills/gluestack-nativewind/SKILL.md +8 -0
  37. package/plugins/lisa-expo-copilot/skills/gluestack-nativewind/scripts/test_validate_styling.py +240 -0
  38. package/plugins/lisa-expo-copilot/skills/gluestack-nativewind/scripts/validate_styling.py +109 -0
  39. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  40. package/plugins/lisa-expo-cursor/skills/atomic-design-gluestack/SKILL.md +8 -0
  41. package/plugins/lisa-expo-cursor/skills/atomic-design-gluestack/scripts/test_validate_atomic_structure.py +240 -0
  42. package/plugins/lisa-expo-cursor/skills/atomic-design-gluestack/scripts/validate_atomic_structure.py +109 -0
  43. package/plugins/lisa-expo-cursor/skills/gluestack-nativewind/SKILL.md +8 -0
  44. package/plugins/lisa-expo-cursor/skills/gluestack-nativewind/scripts/test_validate_styling.py +240 -0
  45. package/plugins/lisa-expo-cursor/skills/gluestack-nativewind/scripts/validate_styling.py +109 -0
  46. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  47. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  48. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  49. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  50. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  51. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  52. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  53. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  54. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  55. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  56. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  57. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  58. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  59. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  60. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  61. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  62. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  63. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  64. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  65. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  66. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  67. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  68. package/plugins/lisa-rails-agy/plugin.json +1 -1
  69. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  70. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  71. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  72. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  73. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  74. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  75. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  76. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  77. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  78. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  79. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  80. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  81. package/plugins/src/base/hooks/install-pkgs.sh +26 -1
  82. package/plugins/src/expo/skills/atomic-design-gluestack/SKILL.md +8 -0
  83. package/plugins/src/expo/skills/atomic-design-gluestack/scripts/test_validate_atomic_structure.py +240 -0
  84. package/plugins/src/expo/skills/atomic-design-gluestack/scripts/validate_atomic_structure.py +109 -0
  85. package/plugins/src/expo/skills/gluestack-nativewind/SKILL.md +8 -0
  86. package/plugins/src/expo/skills/gluestack-nativewind/scripts/test_validate_styling.py +240 -0
  87. package/plugins/src/expo/skills/gluestack-nativewind/scripts/validate_styling.py +109 -0
@@ -7,7 +7,33 @@ set -uo pipefail
7
7
  repo_root="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
8
8
  cd "$repo_root" 2>/dev/null || exit 0
9
9
 
10
- if [ -d "node_modules" ] || [ ! -f "package.json" ]; then
10
+ if [ ! -f "package.json" ]; then
11
+ exit 0
12
+ fi
13
+
14
+ link_primary_worktree_node_modules() {
15
+ [ ! -e "node_modules" ] && [ ! -L "node_modules" ] || return 1
16
+
17
+ case "$repo_root" in
18
+ */.claude/worktrees/*)
19
+ primary_root="${repo_root%%/.claude/worktrees/*}"
20
+ ;;
21
+ *)
22
+ return 1
23
+ ;;
24
+ esac
25
+
26
+ [ "$primary_root" != "$repo_root" ] || return 1
27
+ [ -d "$primary_root/node_modules" ] || return 1
28
+
29
+ ln -s "$primary_root/node_modules" "node_modules"
30
+ }
31
+
32
+ if [ -d "node_modules" ]; then
33
+ exit 0
34
+ fi
35
+
36
+ if link_primary_worktree_node_modules && [ -d "node_modules" ]; then
11
37
  exit 0
12
38
  fi
13
39
 
@@ -23,7 +23,7 @@ export const LisaSessionBootstrap = async ({
23
23
  worktree: string;
24
24
  }) => {
25
25
  const root = worktree;
26
- const { existsSync, mkdirSync, readFileSync, writeFileSync } =
26
+ const { existsSync, mkdirSync, readFileSync, symlinkSync, writeFileSync } =
27
27
  await import("node:fs");
28
28
 
29
29
  // install-pkgs: bootstrap dependencies when they're missing.
@@ -32,16 +32,29 @@ export const LisaSessionBootstrap = async ({
32
32
  existsSync(`${root}/package.json`) &&
33
33
  !existsSync(`${root}/node_modules`)
34
34
  ) {
35
+ let linkedPrimaryNodeModules = false;
36
+ const marker = "/.claude/worktrees/";
37
+ if (root.includes(marker)) {
38
+ const primaryRoot = root.slice(0, root.indexOf(marker));
39
+ const primaryNodeModules = `${primaryRoot}/node_modules`;
40
+ if (primaryRoot && existsSync(primaryNodeModules)) {
41
+ symlinkSync(primaryNodeModules, `${root}/node_modules`);
42
+ linkedPrimaryNodeModules = true;
43
+ }
44
+ }
45
+
35
46
  const has = (f: string) => existsSync(`${root}/${f}`);
36
47
  const install = async (cmd: string) => {
37
48
  if (Bun.which(cmd)) {
38
49
  await $`${cmd} install`.cwd(root).quiet().nothrow();
39
50
  }
40
51
  };
41
- if (has("bun.lockb") || has("bun.lock")) await install("bun");
42
- else if (has("pnpm-lock.yaml")) await install("pnpm");
43
- else if (has("yarn.lock")) await install("yarn");
44
- else await install("npm");
52
+ if (!linkedPrimaryNodeModules) {
53
+ if (has("bun.lockb") || has("bun.lock")) await install("bun");
54
+ else if (has("pnpm-lock.yaml")) await install("pnpm");
55
+ else if (has("yarn.lock")) await install("yarn");
56
+ else await install("npm");
57
+ }
45
58
  }
46
59
  } catch {
47
60
  // fail open — never block startup on a dependency-install error
package/package.json CHANGED
@@ -91,7 +91,7 @@
91
91
  "ws": ">=8.20.1"
92
92
  },
93
93
  "name": "@codyswann/lisa",
94
- "version": "2.178.2",
94
+ "version": "2.178.4",
95
95
  "description": "Claude Code governance framework that applies guardrails, guidance, and automated enforcement to projects",
96
96
  "main": "dist/index.js",
97
97
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "2.178.2",
3
+ "version": "2.178.4",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "2.178.2",
3
+ "version": "2.178.4",
4
4
  "description": "Universal governance: agents, skills, commands, hooks, and rules for all projects.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -2,12 +2,37 @@
2
2
  # This file is managed by Lisa.
3
3
  # Do not edit directly — changes will be overwritten on the next `lisa` run.
4
4
 
5
+ link_primary_worktree_node_modules() {
6
+ [ -f "package.json" ] || return 1
7
+ [ ! -e "node_modules" ] && [ ! -L "node_modules" ] || return 1
8
+
9
+ repo_root="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
10
+ case "$repo_root" in
11
+ */.claude/worktrees/*)
12
+ primary_root="${repo_root%%/.claude/worktrees/*}"
13
+ ;;
14
+ *)
15
+ return 1
16
+ ;;
17
+ esac
18
+
19
+ [ "$primary_root" != "$repo_root" ] || return 1
20
+ [ -d "$primary_root/node_modules" ] || return 1
21
+
22
+ ln -s "$primary_root/node_modules" "node_modules"
23
+ echo "Linked node_modules from primary worktree: $primary_root/node_modules"
24
+ }
25
+
5
26
  # Only run package installation when node_modules are missing.
6
27
  # This covers remote environments, new worktrees, fresh clones, and CI.
7
28
  if [ -d "node_modules" ]; then
8
29
  exit 0
9
30
  fi
10
31
 
32
+ if link_primary_worktree_node_modules && [ -d "node_modules" ]; then
33
+ exit 0
34
+ fi
35
+
11
36
  # Detect the package manager this project wants, honoring explicit opt-outs.
12
37
  # Precedence: packageManager field > engines "please-use-<pm>" sentinel >
13
38
  # lockfile presence (minus any PM the engines forbid) > npm default.
@@ -88,4 +113,4 @@ if [ -n "$CHROME_PATH" ]; then
88
113
  echo "Chromium installed at: $CHROME_PATH"
89
114
  fi
90
115
 
91
- exit 0
116
+ exit 0
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "2.178.2",
3
+ "version": "2.178.4",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "2.178.2",
3
+ "version": "2.178.4",
4
4
  "description": "AWS CDK-specific plugin",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "2.178.2",
3
+ "version": "2.178.4",
4
4
  "description": "AWS CDK-specific Lisa plugin.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "2.178.2",
3
+ "version": "2.178.4",
4
4
  "description": "AWS CDK-specific plugin",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "2.178.2",
3
+ "version": "2.178.4",
4
4
  "description": "AWS CDK-specific plugin",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "2.178.2",
3
+ "version": "2.178.4",
4
4
  "description": "AWS CDK-specific plugin",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "2.178.2",
3
+ "version": "2.178.4",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -2,12 +2,37 @@
2
2
  # This file is managed by Lisa.
3
3
  # Do not edit directly — changes will be overwritten on the next `lisa` run.
4
4
 
5
+ link_primary_worktree_node_modules() {
6
+ [ -f "package.json" ] || return 1
7
+ [ ! -e "node_modules" ] && [ ! -L "node_modules" ] || return 1
8
+
9
+ repo_root="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
10
+ case "$repo_root" in
11
+ */.claude/worktrees/*)
12
+ primary_root="${repo_root%%/.claude/worktrees/*}"
13
+ ;;
14
+ *)
15
+ return 1
16
+ ;;
17
+ esac
18
+
19
+ [ "$primary_root" != "$repo_root" ] || return 1
20
+ [ -d "$primary_root/node_modules" ] || return 1
21
+
22
+ ln -s "$primary_root/node_modules" "node_modules"
23
+ echo "Linked node_modules from primary worktree: $primary_root/node_modules"
24
+ }
25
+
5
26
  # Only run package installation when node_modules are missing.
6
27
  # This covers remote environments, new worktrees, fresh clones, and CI.
7
28
  if [ -d "node_modules" ]; then
8
29
  exit 0
9
30
  fi
10
31
 
32
+ if link_primary_worktree_node_modules && [ -d "node_modules" ]; then
33
+ exit 0
34
+ fi
35
+
11
36
  # Detect the package manager this project wants, honoring explicit opt-outs.
12
37
  # Precedence: packageManager field > engines "please-use-<pm>" sentinel >
13
38
  # lockfile presence (minus any PM the engines forbid) > npm default.
@@ -88,4 +113,4 @@ if [ -n "$CHROME_PATH" ]; then
88
113
  echo "Chromium installed at: $CHROME_PATH"
89
114
  fi
90
115
 
91
- exit 0
116
+ exit 0
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "2.178.2",
3
+ "version": "2.178.4",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -2,12 +2,37 @@
2
2
  # This file is managed by Lisa.
3
3
  # Do not edit directly — changes will be overwritten on the next `lisa` run.
4
4
 
5
+ link_primary_worktree_node_modules() {
6
+ [ -f "package.json" ] || return 1
7
+ [ ! -e "node_modules" ] && [ ! -L "node_modules" ] || return 1
8
+
9
+ repo_root="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
10
+ case "$repo_root" in
11
+ */.claude/worktrees/*)
12
+ primary_root="${repo_root%%/.claude/worktrees/*}"
13
+ ;;
14
+ *)
15
+ return 1
16
+ ;;
17
+ esac
18
+
19
+ [ "$primary_root" != "$repo_root" ] || return 1
20
+ [ -d "$primary_root/node_modules" ] || return 1
21
+
22
+ ln -s "$primary_root/node_modules" "node_modules"
23
+ echo "Linked node_modules from primary worktree: $primary_root/node_modules"
24
+ }
25
+
5
26
  # Only run package installation when node_modules are missing.
6
27
  # This covers remote environments, new worktrees, fresh clones, and CI.
7
28
  if [ -d "node_modules" ]; then
8
29
  exit 0
9
30
  fi
10
31
 
32
+ if link_primary_worktree_node_modules && [ -d "node_modules" ]; then
33
+ exit 0
34
+ fi
35
+
11
36
  # Detect the package manager this project wants, honoring explicit opt-outs.
12
37
  # Precedence: packageManager field > engines "please-use-<pm>" sentinel >
13
38
  # lockfile presence (minus any PM the engines forbid) > npm default.
@@ -88,4 +113,4 @@ if [ -n "$CHROME_PATH" ]; then
88
113
  echo "Chromium installed at: $CHROME_PATH"
89
114
  fi
90
115
 
91
- exit 0
116
+ exit 0
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-expo",
3
- "version": "2.178.2",
3
+ "version": "2.178.4",
4
4
  "description": "Expo/React Native-specific skills, agents, rules, and MCP servers",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-expo",
3
- "version": "2.178.2",
3
+ "version": "2.178.4",
4
4
  "description": "Expo and React Native-specific skills, agents, rules, and MCP servers.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -10,6 +10,14 @@ description: |
10
10
 
11
11
  This skill enforces Brad Frost's atomic design methodology adapted for React Native/Expo projects using Gluestack UI v3 + NativeWind v4. It provides clear guidelines for component categorization, directory structure, composition patterns, and testing strategies for each atomic level.
12
12
 
13
+ ## Sealed Design System Guard
14
+
15
+ Before applying this skill, check whether the target project has a sealed or closed design system. Common markers include `.claude/rules/use-the-design-library.md`, `.claude/rules/figma-design-system.md`, design-system ESLint rules, or a project-owned `@/components/atoms` barrel paired with design-system docs/configuration.
16
+
17
+ If a sealed design system is present, defer to the project's design-library rules and skills instead, such as `use-the-design-library` and `add-design-library-item`. Keep the atomic hierarchy concepts only when they match the local seal. Do not redefine the atom library as `@/components/ui`, use Gluestack compound trees as canonical atoms, or recommend `className` and raw Gluestack tokens where the project seal forbids them.
18
+
19
+ For open Gluestack UI v3 + NativeWind v4 projects without those seal markers, continue with the guidance below.
20
+
13
21
  ## Core Principles
14
22
 
15
23
  ### The Atomic Hierarchy
@@ -0,0 +1,240 @@
1
+ """
2
+ Tests for validate_atomic_structure.py seal detection helpers.
3
+
4
+ Covers:
5
+ - find_project_roots: should return ALL ancestor roots (not just first)
6
+ - has_design_system_eslint_rule: should only match actual rule/plugin syntax
7
+ - has_design_system_eslint_rule: should check .eslintrc.yaml/.yml, eslint.config.mts/.cts
8
+ - has_design_system_eslint_rule: should parse package.json eslintConfig field
9
+ """
10
+
11
+ import json
12
+ import sys
13
+ import tempfile
14
+ import unittest
15
+ from pathlib import Path
16
+
17
+ # Allow importing the module under test
18
+ sys.path.insert(0, str(Path(__file__).parent))
19
+ from validate_atomic_structure import (
20
+ find_project_roots,
21
+ has_design_system_eslint_rule,
22
+ has_sealed_design_system,
23
+ )
24
+
25
+
26
+ class TestFindProjectRoots(unittest.TestCase):
27
+ """find_project_roots should return ALL package boundaries, not just the nearest."""
28
+
29
+ def test_returns_all_ancestor_roots_in_monorepo(self) -> None:
30
+ """In a monorepo, both the inner package root and the workspace root are returned."""
31
+ with tempfile.TemporaryDirectory() as tmp:
32
+ workspace = Path(tmp) / "workspace"
33
+ inner = workspace / "apps" / "mobile"
34
+ inner.mkdir(parents=True)
35
+
36
+ # workspace root: has .git
37
+ (workspace / ".git").mkdir()
38
+ # inner package: has package.json
39
+ (inner / "package.json").write_text("{}", encoding="utf-8")
40
+
41
+ source_file = inner / "src" / "components"
42
+ source_file.mkdir(parents=True)
43
+
44
+ roots = find_project_roots(str(source_file))
45
+ # Both the inner package root and the workspace root should be returned.
46
+ # Use .resolve() to handle symlinked temp dirs (e.g. /var → /private/var on macOS).
47
+ self.assertIn(inner.resolve(), roots)
48
+ self.assertIn(workspace.resolve(), roots)
49
+
50
+ def test_returns_single_root_for_simple_project(self) -> None:
51
+ """A flat project returns its single root."""
52
+ with tempfile.TemporaryDirectory() as tmp:
53
+ project = Path(tmp) / "project"
54
+ project.mkdir()
55
+ (project / "package.json").write_text("{}", encoding="utf-8")
56
+ (project / ".git").mkdir()
57
+
58
+ roots = find_project_roots(str(project / "src"))
59
+ self.assertEqual(len(roots), 1)
60
+ # Use .resolve() to handle symlinked temp dirs (e.g. /var → /private/var on macOS).
61
+ self.assertEqual(roots[0], project.resolve())
62
+
63
+ def test_falls_back_to_current_directory_when_no_root_found(self) -> None:
64
+ """When no package.json or .git exists, returns the starting path."""
65
+ with tempfile.TemporaryDirectory() as tmp:
66
+ orphan = Path(tmp) / "orphan" / "dir"
67
+ orphan.mkdir(parents=True)
68
+ roots = find_project_roots(str(orphan))
69
+ self.assertEqual(len(roots), 1)
70
+
71
+
72
+ class TestHasDesignSystemEslintRule(unittest.TestCase):
73
+ """has_design_system_eslint_rule should match actual rule/plugin identifiers only."""
74
+
75
+ def test_ignores_comment_containing_design_system_substring(self) -> None:
76
+ """A comment that mentions 'design-system' without a slash must not trigger the seal."""
77
+ with tempfile.TemporaryDirectory() as tmp:
78
+ root = Path(tmp)
79
+ eslint = root / ".eslintrc.json"
80
+ eslint.write_text(
81
+ '{"rules": {}, "// note": "do not use design-system here"}',
82
+ encoding="utf-8",
83
+ )
84
+ self.assertFalse(
85
+ has_design_system_eslint_rule(root),
86
+ msg="Plain 'design-system' substring in a comment must not match",
87
+ )
88
+
89
+ def test_matches_design_system_slash_rule_syntax(self) -> None:
90
+ """An ESLint rule like 'design-system/no-raw-colors' should trigger the seal."""
91
+ with tempfile.TemporaryDirectory() as tmp:
92
+ root = Path(tmp)
93
+ eslint = root / ".eslintrc.json"
94
+ eslint.write_text(
95
+ '{"rules": {"design-system/no-raw-colors": "error"}}',
96
+ encoding="utf-8",
97
+ )
98
+ self.assertTrue(has_design_system_eslint_rule(root))
99
+
100
+ def test_checks_eslintrc_yaml(self) -> None:
101
+ """.eslintrc.yaml is a valid ESLint config and must be checked."""
102
+ with tempfile.TemporaryDirectory() as tmp:
103
+ root = Path(tmp)
104
+ yaml_config = root / ".eslintrc.yaml"
105
+ yaml_config.write_text(
106
+ 'rules:\n design-system/no-raw-colors: error\n',
107
+ encoding="utf-8",
108
+ )
109
+ self.assertTrue(has_design_system_eslint_rule(root))
110
+
111
+ def test_checks_eslintrc_yml(self) -> None:
112
+ """.eslintrc.yml is a valid ESLint config and must be checked."""
113
+ with tempfile.TemporaryDirectory() as tmp:
114
+ root = Path(tmp)
115
+ yml_config = root / ".eslintrc.yml"
116
+ yml_config.write_text(
117
+ 'rules:\n design-system/no-raw-colors: error\n',
118
+ encoding="utf-8",
119
+ )
120
+ self.assertTrue(has_design_system_eslint_rule(root))
121
+
122
+ def test_checks_eslint_config_mts(self) -> None:
123
+ """eslint.config.mts is a valid ESLint flat-config entry point and must be checked."""
124
+ with tempfile.TemporaryDirectory() as tmp:
125
+ root = Path(tmp)
126
+ mts_config = root / "eslint.config.mts"
127
+ mts_config.write_text(
128
+ 'export default [{"rules": {"design-system/no-raw-colors": "error"}}];',
129
+ encoding="utf-8",
130
+ )
131
+ self.assertTrue(has_design_system_eslint_rule(root))
132
+
133
+ def test_checks_eslint_config_cts(self) -> None:
134
+ """eslint.config.cts is a valid ESLint flat-config entry point and must be checked."""
135
+ with tempfile.TemporaryDirectory() as tmp:
136
+ root = Path(tmp)
137
+ cts_config = root / "eslint.config.cts"
138
+ cts_config.write_text(
139
+ 'module.exports = [{"rules": {"design-system/no-raw-colors": "error"}}];',
140
+ encoding="utf-8",
141
+ )
142
+ self.assertTrue(has_design_system_eslint_rule(root))
143
+
144
+ def test_checks_package_json_eslintconfig_field(self) -> None:
145
+ """The eslintConfig key in package.json must be checked for design-system rules."""
146
+ with tempfile.TemporaryDirectory() as tmp:
147
+ root = Path(tmp)
148
+ pkg = {
149
+ "name": "my-app",
150
+ "eslintConfig": {
151
+ "rules": {"design-system/no-raw-colors": "error"}
152
+ },
153
+ }
154
+ (root / "package.json").write_text(json.dumps(pkg), encoding="utf-8")
155
+ self.assertTrue(has_design_system_eslint_rule(root))
156
+
157
+ def test_package_json_non_eslintconfig_does_not_trigger(self) -> None:
158
+ """A 'design-system' substring in package.json outside eslintConfig must not match."""
159
+ with tempfile.TemporaryDirectory() as tmp:
160
+ root = Path(tmp)
161
+ pkg = {
162
+ "name": "my-app",
163
+ "dependencies": {"design-system": "^1.0.0"},
164
+ }
165
+ (root / "package.json").write_text(json.dumps(pkg), encoding="utf-8")
166
+ self.assertFalse(
167
+ has_design_system_eslint_rule(root),
168
+ msg="'design-system' in dependencies must not trigger the ESLint seal check",
169
+ )
170
+
171
+
172
+ class TestHasSealedDesignSystem(unittest.TestCase):
173
+ """has_sealed_design_system seal-detection logic."""
174
+
175
+ def test_seal_marker_alone_is_sufficient(self) -> None:
176
+ """A seal marker without an atom barrel or .claude/rules directory seals the project."""
177
+ with tempfile.TemporaryDirectory() as tmp:
178
+ project = Path(tmp) / "project"
179
+ project.mkdir()
180
+ (project / ".git").mkdir()
181
+ docs = project / "docs"
182
+ docs.mkdir()
183
+ (docs / "design-system-rfc.md").write_text("", encoding="utf-8")
184
+ # Deliberately NO atom barrel and NO .claude/rules directory.
185
+
186
+ self.assertTrue(
187
+ has_sealed_design_system(str(project)),
188
+ msg="A single seal marker must be sufficient to detect a sealed design system",
189
+ )
190
+
191
+ def test_atom_barrel_alone_is_sufficient(self) -> None:
192
+ """A project exposing only an atoms barrel is treated as having a sealed design system."""
193
+ with tempfile.TemporaryDirectory() as tmp:
194
+ project = Path(tmp) / "project"
195
+ project.mkdir()
196
+ (project / ".git").mkdir()
197
+ atoms = project / "src" / "components" / "atoms"
198
+ atoms.mkdir(parents=True)
199
+ (atoms / "index.ts").write_text("", encoding="utf-8")
200
+ # Deliberately NO seal marker and NO .claude/rules directory.
201
+
202
+ self.assertTrue(
203
+ has_sealed_design_system(str(project)),
204
+ msg="An atoms barrel alone must be sufficient to detect a sealed design system",
205
+ )
206
+
207
+
208
+ class TestHasSealedDesignSystemMonorepo(unittest.TestCase):
209
+ """has_sealed_design_system should find seal markers in ancestor (workspace) roots."""
210
+
211
+ def test_finds_seal_marker_at_workspace_root_not_inner_package(self) -> None:
212
+ """Seal markers placed at the workspace root must be respected for inner packages."""
213
+ with tempfile.TemporaryDirectory() as tmp:
214
+ workspace = Path(tmp) / "workspace"
215
+ inner = workspace / "apps" / "mobile"
216
+ inner.mkdir(parents=True)
217
+
218
+ # workspace root: .git + seal marker
219
+ (workspace / ".git").mkdir()
220
+ rules_dir = workspace / ".claude" / "rules"
221
+ rules_dir.mkdir(parents=True)
222
+ (rules_dir / "use-the-design-library.md").write_text("", encoding="utf-8")
223
+
224
+ # inner package: package.json + atom barrel
225
+ (inner / "package.json").write_text("{}", encoding="utf-8")
226
+ atoms = inner / "components" / "atoms"
227
+ atoms.mkdir(parents=True)
228
+ (atoms / "index.ts").write_text("", encoding="utf-8")
229
+
230
+ target_dir = inner / "src" / "components" / "atoms"
231
+ target_dir.mkdir(parents=True)
232
+
233
+ self.assertTrue(
234
+ has_sealed_design_system(str(target_dir)),
235
+ msg="Seal marker at workspace root must be detected even for nested packages",
236
+ )
237
+
238
+
239
+ if __name__ == "__main__":
240
+ unittest.main()