@codyswann/lisa 1.15.0 → 1.16.0
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.
- package/all/copy-overwrite/.claude/hooks/enforce-plan-rules.sh +15 -0
- package/all/copy-overwrite/.claude/hooks/track-plan-sessions.sh +155 -0
- package/all/copy-overwrite/.claude/rules/plan.md +29 -0
- package/all/copy-overwrite/.claude/settings.json +30 -0
- package/all/copy-overwrite/CLAUDE.md +2 -9
- package/expo/copy-overwrite/jest.expo.ts +15 -12
- package/expo/copy-overwrite/knip.json +1 -1
- package/expo/copy-overwrite/tsconfig.expo.json +4 -0
- package/expo/copy-overwrite/tsconfig.json +1 -1
- package/expo/create-only/babel.config.js +27 -0
- package/expo/create-only/jest.config.local.ts +34 -0
- package/expo/create-only/jest.config.react-native-mock.js +88 -0
- package/expo/create-only/jest.setup.pre.js +106 -0
- package/expo/create-only/jest.setup.ts +118 -0
- package/package.json +1 -1
- package/typescript/copy-contents/.husky/pre-push +5 -1
- package/typescript/copy-overwrite/.claude/settings.json +10 -0
- package/typescript/copy-overwrite/eslint.base.ts +1 -1
- package/typescript/copy-overwrite/eslint.ignore.config.json +1 -0
- package/typescript/copy-overwrite/jest.base.ts +1 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Reinjects plan-mode rules on every prompt when Claude is in plan mode.
|
|
3
|
+
# Wired as a UserPromptSubmit hook in .claude/settings.json.
|
|
4
|
+
|
|
5
|
+
INPUT=$(cat)
|
|
6
|
+
PERMISSION_MODE=$(echo "$INPUT" | jq -r '.permission_mode // "default"')
|
|
7
|
+
|
|
8
|
+
if [ "$PERMISSION_MODE" = "plan" ]; then
|
|
9
|
+
PLAN_RULES="$CLAUDE_PROJECT_DIR/.claude/rules/plan.md"
|
|
10
|
+
if [ -f "$PLAN_RULES" ]; then
|
|
11
|
+
echo "PLAN MODE RULES (reinforced):"
|
|
12
|
+
cat "$PLAN_RULES"
|
|
13
|
+
fi
|
|
14
|
+
fi
|
|
15
|
+
exit 0
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# track-plan-sessions.sh - Tracks which sessions work on each plan file
|
|
4
|
+
#
|
|
5
|
+
# Triggered by two hooks:
|
|
6
|
+
# 1. PostToolUse (Write|Edit) - Detects plan file writes via absolute path comparison,
|
|
7
|
+
# stamps session ID into the plan's ## Sessions table, and saves a session-specific
|
|
8
|
+
# marker file so subsequent UserPromptSubmit events can reliably find the active plan.
|
|
9
|
+
# 2. UserPromptSubmit - Finds the active plan file by checking for a session-specific
|
|
10
|
+
# marker file first (set by PostToolUse), falling back to the most recently CREATED
|
|
11
|
+
# .md file in plans/ (ls -tU on macOS sorts by birth time). This avoids the mtime bug
|
|
12
|
+
# where another plan file's modification time (e.g., from a hook writing a session ID
|
|
13
|
+
# to it, or format-on-edit touching it) could cause the wrong file to appear "newest."
|
|
14
|
+
#
|
|
15
|
+
# Marker files: $PLANS_DIR/.active-plan-<session-id> contain the absolute path to the
|
|
16
|
+
# active plan file. Stale markers (>24h) are cleaned up on each invocation.
|
|
17
|
+
#
|
|
18
|
+
# Debug logging: All key decisions are logged to $PLANS_DIR/.track-plan-debug.log for
|
|
19
|
+
# diagnostics if session IDs land in the wrong plan file.
|
|
20
|
+
#
|
|
21
|
+
# Input (via stdin): JSON with session_id, permission_mode, hook_event_name, etc.
|
|
22
|
+
#
|
|
23
|
+
|
|
24
|
+
set -euo pipefail
|
|
25
|
+
|
|
26
|
+
INPUT=$(cat)
|
|
27
|
+
|
|
28
|
+
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // empty')
|
|
29
|
+
PERMISSION_MODE=$(echo "$INPUT" | jq -r '.permission_mode // "default"')
|
|
30
|
+
HOOK_EVENT=$(echo "$INPUT" | jq -r '.hook_event_name // empty')
|
|
31
|
+
|
|
32
|
+
# Session ID is required
|
|
33
|
+
if [[ -z "$SESSION_ID" ]]; then
|
|
34
|
+
exit 0
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
# Resolve plans directory from settings (default ./plans)
|
|
38
|
+
PLANS_DIR="./plans"
|
|
39
|
+
SETTINGS_FILE="${CLAUDE_PROJECT_DIR:-.}/.claude/settings.json"
|
|
40
|
+
if [[ -f "$SETTINGS_FILE" ]]; then
|
|
41
|
+
CONFIGURED_DIR=$(jq -r '.plansDirectory // empty' "$SETTINGS_FILE")
|
|
42
|
+
if [[ -n "$CONFIGURED_DIR" ]]; then
|
|
43
|
+
PLANS_DIR="$CONFIGURED_DIR"
|
|
44
|
+
fi
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
# Debug logging
|
|
48
|
+
DEBUG_LOG="$PLANS_DIR/.track-plan-debug.log"
|
|
49
|
+
|
|
50
|
+
log_debug() {
|
|
51
|
+
printf '[%s] [%s] [%s] %s\n' \
|
|
52
|
+
"$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \
|
|
53
|
+
"$SESSION_ID" \
|
|
54
|
+
"$HOOK_EVENT" \
|
|
55
|
+
"$1" >> "$DEBUG_LOG" 2>/dev/null || true
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
# Session-specific marker file for reliable active-plan detection
|
|
59
|
+
MARKER_FILE="$PLANS_DIR/.active-plan-${SESSION_ID}"
|
|
60
|
+
|
|
61
|
+
# Clean stale marker files older than 24h
|
|
62
|
+
find "$PLANS_DIR" -name ".active-plan-*" -mmin +1440 -delete 2>/dev/null || true
|
|
63
|
+
|
|
64
|
+
PLAN_FILE=""
|
|
65
|
+
RESOLUTION_METHOD=""
|
|
66
|
+
|
|
67
|
+
if [[ "$HOOK_EVENT" == "PostToolUse" ]]; then
|
|
68
|
+
# Trigger A: Plan file was written/edited
|
|
69
|
+
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
|
|
70
|
+
|
|
71
|
+
if [[ -z "$FILE_PATH" ]]; then
|
|
72
|
+
log_debug "PostToolUse: no file_path in tool_input, exiting"
|
|
73
|
+
exit 0
|
|
74
|
+
fi
|
|
75
|
+
|
|
76
|
+
# Resolve PLANS_DIR to absolute path for comparison
|
|
77
|
+
ABS_PLANS_DIR=$(cd "$PLANS_DIR" 2>/dev/null && pwd)
|
|
78
|
+
|
|
79
|
+
if [[ -z "$ABS_PLANS_DIR" ]]; then
|
|
80
|
+
log_debug "PostToolUse: could not resolve PLANS_DIR=$PLANS_DIR to absolute path, exiting"
|
|
81
|
+
exit 0
|
|
82
|
+
fi
|
|
83
|
+
|
|
84
|
+
# Check if the written file is in the plans directory
|
|
85
|
+
if [[ "$FILE_PATH" == "$ABS_PLANS_DIR"/* ]]; then
|
|
86
|
+
PLAN_FILE="$FILE_PATH"
|
|
87
|
+
RESOLUTION_METHOD="PostToolUse-direct"
|
|
88
|
+
log_debug "PostToolUse: matched plan file=$PLAN_FILE"
|
|
89
|
+
|
|
90
|
+
# Save marker so UserPromptSubmit can find this plan reliably
|
|
91
|
+
echo "$PLAN_FILE" > "$MARKER_FILE"
|
|
92
|
+
log_debug "PostToolUse: saved marker file=$MARKER_FILE"
|
|
93
|
+
else
|
|
94
|
+
log_debug "PostToolUse: file_path=$FILE_PATH not in plans dir=$ABS_PLANS_DIR, exiting"
|
|
95
|
+
exit 0
|
|
96
|
+
fi
|
|
97
|
+
|
|
98
|
+
elif [[ "$HOOK_EVENT" == "UserPromptSubmit" ]]; then
|
|
99
|
+
# Trigger B: Find the active plan file
|
|
100
|
+
# Priority 1: Session-specific marker file (reliable — set by PostToolUse when plan was written)
|
|
101
|
+
if [[ -f "$MARKER_FILE" ]]; then
|
|
102
|
+
PLAN_FILE=$(cat "$MARKER_FILE")
|
|
103
|
+
RESOLUTION_METHOD="marker-file"
|
|
104
|
+
log_debug "UserPromptSubmit: resolved via marker file=$PLAN_FILE"
|
|
105
|
+
else
|
|
106
|
+
# Priority 2: Most recently CREATED file (ls -tU on macOS sorts by birth time)
|
|
107
|
+
PLAN_FILE=$(ls -tU "$PLANS_DIR"/*.md 2>/dev/null | head -1)
|
|
108
|
+
RESOLUTION_METHOD="fallback-ls-tU"
|
|
109
|
+
log_debug "UserPromptSubmit: no marker file, fallback ls -tU resolved=$PLAN_FILE"
|
|
110
|
+
fi
|
|
111
|
+
|
|
112
|
+
if [[ -z "$PLAN_FILE" || ! -f "$PLAN_FILE" ]]; then
|
|
113
|
+
log_debug "UserPromptSubmit: no valid plan file found, exiting"
|
|
114
|
+
exit 0
|
|
115
|
+
fi
|
|
116
|
+
else
|
|
117
|
+
exit 0
|
|
118
|
+
fi
|
|
119
|
+
|
|
120
|
+
# Verify the plan file exists
|
|
121
|
+
if [[ ! -f "$PLAN_FILE" ]]; then
|
|
122
|
+
log_debug "plan file=$PLAN_FILE does not exist, exiting"
|
|
123
|
+
exit 0
|
|
124
|
+
fi
|
|
125
|
+
|
|
126
|
+
# Check if session ID already exists in the file (dedup)
|
|
127
|
+
if grep -qF "$SESSION_ID" "$PLAN_FILE" 2>/dev/null; then
|
|
128
|
+
log_debug "dedup: session ID already in $PLAN_FILE (resolved via $RESOLUTION_METHOD), skipping write"
|
|
129
|
+
exit 0
|
|
130
|
+
fi
|
|
131
|
+
|
|
132
|
+
# Determine phase from permission_mode
|
|
133
|
+
PHASE=$( [[ "$PERMISSION_MODE" == "plan" ]] && echo "plan" || echo "implement" )
|
|
134
|
+
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
135
|
+
|
|
136
|
+
# Check if ## Sessions section exists
|
|
137
|
+
if grep -q "^## Sessions" "$PLAN_FILE" 2>/dev/null; then
|
|
138
|
+
# Append row to existing table
|
|
139
|
+
printf '| %s | %s | %s |\n' "$SESSION_ID" "$TIMESTAMP" "$PHASE" >> "$PLAN_FILE"
|
|
140
|
+
else
|
|
141
|
+
# Create ## Sessions section at end of file
|
|
142
|
+
{
|
|
143
|
+
echo ""
|
|
144
|
+
echo "## Sessions"
|
|
145
|
+
echo ""
|
|
146
|
+
echo "<!-- Auto-maintained by track-plan-sessions.sh -->"
|
|
147
|
+
echo "| Session ID | First Seen | Phase |"
|
|
148
|
+
echo "|------------|------------|-------|"
|
|
149
|
+
printf '| %s | %s | %s |\n' "$SESSION_ID" "$TIMESTAMP" "$PHASE"
|
|
150
|
+
} >> "$PLAN_FILE"
|
|
151
|
+
fi
|
|
152
|
+
|
|
153
|
+
log_debug "wrote session to $PLAN_FILE (resolved via $RESOLUTION_METHOD, phase=$PHASE)"
|
|
154
|
+
|
|
155
|
+
exit 0
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Plan Mode Rules
|
|
2
|
+
|
|
3
|
+
These rules are enforced whenever Claude is in plan mode. They are loaded at session start via `.claude/rules/` and reinforced on every prompt via the `enforce-plan-rules.sh` `UserPromptSubmit` hook.
|
|
4
|
+
|
|
5
|
+
## Required Behaviors
|
|
6
|
+
|
|
7
|
+
When making a plan:
|
|
8
|
+
|
|
9
|
+
- Always determine which skills should be used during execution of the plan and include them in the plan
|
|
10
|
+
- Always make sure you understand the correct versions of third party libraries
|
|
11
|
+
- Always save the plan with a name befitting the actual plan contents
|
|
12
|
+
- Always look for code that can be reused for implementation
|
|
13
|
+
- The plan MUST including written instructions to create a task list using TaskCreate for each task. The list should contain items related to the plan and specify that subagents should handle as many in parallel as possible. The following should always be included in the task list
|
|
14
|
+
- update/add/remove tests, containing the tests that need to get updated, added or removed
|
|
15
|
+
- update/add/remove documentation (jsdocs, markdown files, etc), containing the documentation that need to get updated, added or removed
|
|
16
|
+
- archive the plan (to be completed after all other tasks have been completed). This task should explcitly say to:
|
|
17
|
+
- create a folder named <plan-name> in ./plans/completed
|
|
18
|
+
- rename this plan to a name befitting the actual plan contents
|
|
19
|
+
- move it into ./plans/completed/<plan-name>
|
|
20
|
+
- read the session ids from ./plans/completed/<plan-name>
|
|
21
|
+
- For each session id, move the ~/.claude/tasks/<session-id> directory to ./plans/completed/<plan-name>/tasks
|
|
22
|
+
- If you're on a protected branch (dev, staging, main), create a new branch named based on the nature of the project and include in the plan pull requests should go to the protected branch you bracnehd from.
|
|
23
|
+
- If you're on a non-protected branch with an open pull request, submit pushes to the open pull request
|
|
24
|
+
- If you're on a non-protected branch with no existing PR, clarify which protected branch to open the pull request to.
|
|
25
|
+
- If referencing a ticket (jira, linear, etc), always include the ticket url in your plan
|
|
26
|
+
- If referencing a ticket (jira, linear, etc), always update the ticket with the branch you're working off of
|
|
27
|
+
- If referencing a ticket (jira, linear, etc), always add a comment to the ticket with the finalized plan
|
|
28
|
+
- The `## Sessions` section in plan files is auto-maintained by the `track-plan-sessions.sh` hook — do not manually edit it
|
|
29
|
+
|
|
@@ -10,9 +10,39 @@
|
|
|
10
10
|
"timeout": 1
|
|
11
11
|
}
|
|
12
12
|
]
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"matcher": "",
|
|
16
|
+
"hooks": [
|
|
17
|
+
{
|
|
18
|
+
"type": "command",
|
|
19
|
+
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/enforce-plan-rules.sh",
|
|
20
|
+
"timeout": 5
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"matcher": "",
|
|
26
|
+
"hooks": [
|
|
27
|
+
{
|
|
28
|
+
"type": "command",
|
|
29
|
+
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/track-plan-sessions.sh",
|
|
30
|
+
"timeout": 5
|
|
31
|
+
}
|
|
32
|
+
]
|
|
13
33
|
}
|
|
14
34
|
],
|
|
15
35
|
"PostToolUse": [
|
|
36
|
+
{
|
|
37
|
+
"matcher": "Write|Edit",
|
|
38
|
+
"hooks": [
|
|
39
|
+
{
|
|
40
|
+
"type": "command",
|
|
41
|
+
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/track-plan-sessions.sh",
|
|
42
|
+
"timeout": 5
|
|
43
|
+
}
|
|
44
|
+
]
|
|
45
|
+
},
|
|
16
46
|
{
|
|
17
47
|
"matcher": "TaskCreate|TaskUpdate",
|
|
18
48
|
"hooks": [
|
|
@@ -16,7 +16,7 @@ Always use project-relative paths rather than absolute paths in documentation an
|
|
|
16
16
|
Always ignore build folders (dist, build, etc) unless specified otherwise
|
|
17
17
|
Always delete and remove old code completely - no deprecation needed
|
|
18
18
|
Always add `GIT_SSH_COMMAND="ssh -o ServerAliveInterval=30 -o ServerAliveCountMax=5" ` when running `git push`
|
|
19
|
-
|
|
19
|
+
Always err on the side of creating a plan before directly executing a coding task
|
|
20
20
|
|
|
21
21
|
Never modify this file (CLAUDE.md) directly. To add a memory or learning, add it to .claude/rules/PROJECT_RULES.md or create a skill with /skill-creator.
|
|
22
22
|
Never commit changes to an environment branch (dev, staging, main) directly. This is enforced by the pre-commit hook.
|
|
@@ -52,12 +52,5 @@ ONLY use ts-ignore as a last resort and confirm with human before doing so
|
|
|
52
52
|
ONLY use ts-expect-error as a last resort and confirm with human before doing so
|
|
53
53
|
ONLY use ts-nocheck as a last resort and confirm with human before doing so
|
|
54
54
|
|
|
55
|
+
Never update CHANGELOG
|
|
55
56
|
|
|
56
|
-
When making a plan, always determine which skills should be used during execution of the plan and include them in the plan
|
|
57
|
-
When making a plan, always make sure you understand the correct versions of third party libraries
|
|
58
|
-
When making a plan, always create a task list of items related to the plan and specify that subagents should handle as many in parallel as possible
|
|
59
|
-
When making a plan, always save the plan with a name befitting the actual plan contents.
|
|
60
|
-
When making a plan, always look for code that can be reused for implementation
|
|
61
|
-
When making a plan, always include a task to update/add/remove documentation
|
|
62
|
-
|
|
63
|
-
Never update CHANGELOG
|
|
@@ -13,8 +13,14 @@
|
|
|
13
13
|
* `window` property, causing "Cannot redefine property: window" errors.
|
|
14
14
|
*
|
|
15
15
|
* Instead, this config manually replicates the preset's resolution,
|
|
16
|
-
* transform, and haste settings
|
|
17
|
-
*
|
|
16
|
+
* transform, and haste settings without any preset setupFiles.
|
|
17
|
+
*
|
|
18
|
+
* `setupFiles` is intentionally empty because `jest-expo/src/preset/setup.js`
|
|
19
|
+
* requires `__DEV__` to be defined before it runs, and `mergeConfigs`
|
|
20
|
+
* concatenates arrays with base entries first — making it impossible for
|
|
21
|
+
* project-local setupFiles to prepend a `__DEV__` definition. Projects
|
|
22
|
+
* should add their own setupFiles in `jest.config.local.ts` with the
|
|
23
|
+
* correct ordering (define globals first, then load jest-expo setup).
|
|
18
24
|
*
|
|
19
25
|
* Coverage collection is scoped to standard Expo source directories
|
|
20
26
|
* rather than a catch-all glob, preventing config files, scripts, and
|
|
@@ -60,8 +66,10 @@ interface ExpoJestOptions {
|
|
|
60
66
|
* @param options.thresholds - Coverage thresholds (merged defaults + project overrides)
|
|
61
67
|
* @returns Jest config object with jsdom environment, babel-jest transform, and React Native resolver
|
|
62
68
|
* @remarks Avoids `jest-expo` preset to prevent jsdom + `react-native/jest/setup.js`
|
|
63
|
-
* incompatibility. Manually configures haste, resolver, transform
|
|
64
|
-
*
|
|
69
|
+
* incompatibility. Manually configures haste, resolver, and transform to match the
|
|
70
|
+
* preset's behavior without the problematic window redefinition. `setupFiles` is
|
|
71
|
+
* empty — projects must provide their own in `jest.config.local.ts` with correct
|
|
72
|
+
* ordering (define `__DEV__` before loading `jest-expo/src/preset/setup.js`).
|
|
65
73
|
*/
|
|
66
74
|
export const getExpoJestConfig = ({
|
|
67
75
|
thresholds = defaultThresholds,
|
|
@@ -72,7 +80,7 @@ export const getExpoJestConfig = ({
|
|
|
72
80
|
platforms: ["android", "ios", "native"],
|
|
73
81
|
},
|
|
74
82
|
resolver: "react-native/jest/resolver.js",
|
|
75
|
-
setupFiles: [
|
|
83
|
+
setupFiles: [],
|
|
76
84
|
transform: {
|
|
77
85
|
"\\.[jt]sx?$": [
|
|
78
86
|
"babel-jest",
|
|
@@ -87,15 +95,10 @@ export const getExpoJestConfig = ({
|
|
|
87
95
|
"^.+\\.(bmp|gif|jpg|jpeg|mp4|png|psd|svg|webp|ttf|otf|woff|woff2)$":
|
|
88
96
|
"jest-expo/src/preset/assetFileTransformer.js",
|
|
89
97
|
},
|
|
90
|
-
testMatch: [
|
|
91
|
-
"<rootDir>/**/*.test.ts",
|
|
92
|
-
"<rootDir>/**/*.test.tsx",
|
|
93
|
-
"<rootDir>/**/__tests__/**/*.ts",
|
|
94
|
-
"<rootDir>/**/__tests__/**/*.tsx",
|
|
95
|
-
],
|
|
98
|
+
testMatch: ["<rootDir>/**/*.test.ts", "<rootDir>/**/*.test.tsx"],
|
|
96
99
|
testPathIgnorePatterns: ["/node_modules/", "/dist/", "/.expo/"],
|
|
97
100
|
transformIgnorePatterns: [
|
|
98
|
-
"node_modules/(?!((jest-)?react-native|@react-native(-community)?)|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|@react-navigation/.*|@sentry/react-native|native-base|react-native-svg|@gluestack-ui/.*|@gluestack-style/.*|nativewind|react-native-css-interop)",
|
|
101
|
+
"node_modules/(?!((jest-)?react-native|@react-native(-community)?)|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|@react-navigation/.*|@sentry/react-native|native-base|react-native-svg|@gluestack-ui/.*|@gluestack-style/.*|nativewind|react-native-css-interop|react-native-reanimated|react-native-worklets|lucide-react-native|@gorhom|@shopify)",
|
|
99
102
|
],
|
|
100
103
|
moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"],
|
|
101
104
|
collectCoverageFrom: [
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"**/metro.config.js",
|
|
54
54
|
"**/babel.config.js"
|
|
55
55
|
],
|
|
56
|
-
"ignoreBinaries": ["audit", "
|
|
56
|
+
"ignoreBinaries": ["ast-grep", "audit", "eas", "maestro", "serve", "source-map-explorer"],
|
|
57
57
|
"ignoreDependencies": [
|
|
58
58
|
"@dnd-kit/modifiers",
|
|
59
59
|
"@expo-google-fonts/inter",
|
|
@@ -4,7 +4,11 @@
|
|
|
4
4
|
"strict": true,
|
|
5
5
|
"jsx": "react-native",
|
|
6
6
|
"baseUrl": "./",
|
|
7
|
+
"noEmit": true,
|
|
8
|
+
"declaration": false,
|
|
9
|
+
"allowImportingTsExtensions": true,
|
|
7
10
|
"paths": {
|
|
11
|
+
"@/graphql/*": ["./generated/*"],
|
|
8
12
|
"@/*": ["./*"]
|
|
9
13
|
},
|
|
10
14
|
"moduleSuffixes": [".ios", ".android", ".native", ".web", ""]
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Babel Configuration
|
|
3
|
+
*
|
|
4
|
+
* Configures Babel for Expo with NativeWind JSX transform support.
|
|
5
|
+
* This file is create-only — Lisa will create it but never overwrite
|
|
6
|
+
* your customizations.
|
|
7
|
+
*
|
|
8
|
+
* @remarks Required by the `jest.expo.ts` babel-jest transform which
|
|
9
|
+
* uses a metro caller config. Without this file, babel-jest cannot
|
|
10
|
+
* resolve the correct presets for React Native compilation.
|
|
11
|
+
* @see https://docs.expo.dev/versions/latest/config/babel/
|
|
12
|
+
* @module babel.config
|
|
13
|
+
*/
|
|
14
|
+
module.exports = function (api) {
|
|
15
|
+
api.cache(true);
|
|
16
|
+
return {
|
|
17
|
+
presets: [
|
|
18
|
+
[
|
|
19
|
+
"babel-preset-expo",
|
|
20
|
+
{
|
|
21
|
+
jsxImportSource: "nativewind",
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
"nativewind/babel",
|
|
25
|
+
],
|
|
26
|
+
};
|
|
27
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Jest Configuration - Project-Local Customizations
|
|
3
|
+
*
|
|
4
|
+
* Add project-specific Jest settings here. This file is create-only,
|
|
5
|
+
* meaning Lisa will create it but never overwrite your customizations.
|
|
6
|
+
*
|
|
7
|
+
* The Expo stack's `jest.expo.ts` provides haste, resolver, transform,
|
|
8
|
+
* and base coverage settings. This file should only contain settings
|
|
9
|
+
* that are project-specific or need to override the base config.
|
|
10
|
+
*
|
|
11
|
+
* @remarks `setupFiles` must define `__DEV__` and other React Native
|
|
12
|
+
* globals before any RN modules load — `jest.setup.pre.js` handles this.
|
|
13
|
+
* @see https://jestjs.io/docs/configuration
|
|
14
|
+
* @module jest.config.local
|
|
15
|
+
*/
|
|
16
|
+
import type { Config } from "jest";
|
|
17
|
+
|
|
18
|
+
const config: Config = {
|
|
19
|
+
setupFiles: ["<rootDir>/jest.setup.pre.js"],
|
|
20
|
+
setupFilesAfterEnv: ["<rootDir>/jest.setup.ts"],
|
|
21
|
+
|
|
22
|
+
// Path aliases matching tsconfig.expo.json paths
|
|
23
|
+
moduleNameMapper: {
|
|
24
|
+
"^@/(.*)$": "<rootDir>/$1",
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
coveragePathIgnorePatterns: [
|
|
28
|
+
"/node_modules/",
|
|
29
|
+
"/generated/",
|
|
30
|
+
"\\.mock\\.(ts|tsx|js|jsx)$",
|
|
31
|
+
],
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export default config;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React Native TurboModule Mock Configuration
|
|
3
|
+
*
|
|
4
|
+
* Provides mock implementations for React Native's TurboModule native
|
|
5
|
+
* modules used in the test environment. These mocks are loaded by
|
|
6
|
+
* `jest.setup.pre.js` via `__turboModuleProxy`.
|
|
7
|
+
*
|
|
8
|
+
* @remarks Projects should add additional module mocks as needed.
|
|
9
|
+
* Only the baseline modules required by React Native core are included
|
|
10
|
+
* here — project-specific native modules (e.g., camera, maps) should
|
|
11
|
+
* be added to the project's copy of this file.
|
|
12
|
+
* @see https://reactnative.dev/docs/the-new-architecture/pillars-turbomodules
|
|
13
|
+
* @module jest.config.react-native-mock
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
// Dynamically read React Native version from package.json
|
|
17
|
+
const fs = require("fs");
|
|
18
|
+
const path = require("path");
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Parses React Native version from package.json for PlatformConstants mock
|
|
22
|
+
* @returns The React Native version object with major, minor, and patch numbers
|
|
23
|
+
* @remarks Falls back to 0.81.4 if parsing fails — update the fallback
|
|
24
|
+
* when upgrading React Native
|
|
25
|
+
*/
|
|
26
|
+
const getReactNativeVersion = () => {
|
|
27
|
+
try {
|
|
28
|
+
const packageJson = JSON.parse(
|
|
29
|
+
fs.readFileSync(path.join(__dirname, "package.json"), "utf8")
|
|
30
|
+
);
|
|
31
|
+
const version = packageJson.dependencies["react-native"];
|
|
32
|
+
// Handle version formats: "0.81.4", "^0.81.4", "~0.81.4"
|
|
33
|
+
const versionMatch = version.match(/(\d+)\.(\d+)\.(\d+)/);
|
|
34
|
+
if (versionMatch) {
|
|
35
|
+
return {
|
|
36
|
+
major: parseInt(versionMatch[1], 10),
|
|
37
|
+
minor: parseInt(versionMatch[2], 10),
|
|
38
|
+
patch: parseInt(versionMatch[3], 10),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
} catch (error) {
|
|
42
|
+
// Fallback to a default version if parsing fails
|
|
43
|
+
console.warn(
|
|
44
|
+
"Failed to parse React Native version from package.json:",
|
|
45
|
+
error
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
// Fallback version
|
|
49
|
+
return { major: 0, minor: 81, patch: 4 };
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
module.exports = {
|
|
53
|
+
PlatformConstants: {
|
|
54
|
+
getConstants: () => ({
|
|
55
|
+
reactNativeVersion: getReactNativeVersion(),
|
|
56
|
+
forceTouchAvailable: false,
|
|
57
|
+
osVersion: "14.4",
|
|
58
|
+
systemName: "iOS",
|
|
59
|
+
interfaceIdiom: "phone",
|
|
60
|
+
}),
|
|
61
|
+
},
|
|
62
|
+
AppState: {
|
|
63
|
+
getConstants: () => ({ initialAppState: "active" }),
|
|
64
|
+
getCurrentAppState: () => Promise.resolve({ app_state: "active" }),
|
|
65
|
+
addListener: () => {},
|
|
66
|
+
addEventListener: () => {},
|
|
67
|
+
removeListeners: () => {},
|
|
68
|
+
removeEventListener: () => {},
|
|
69
|
+
currentState: "active",
|
|
70
|
+
},
|
|
71
|
+
Appearance: {
|
|
72
|
+
getConstants: () => ({ initialColorScheme: "light" }),
|
|
73
|
+
getColorScheme: () => "light",
|
|
74
|
+
setColorScheme: () => {},
|
|
75
|
+
addChangeListener: () => ({ remove: () => {} }),
|
|
76
|
+
removeChangeListener: () => {},
|
|
77
|
+
addListener: () => ({ remove: () => {} }),
|
|
78
|
+
removeListeners: () => {},
|
|
79
|
+
},
|
|
80
|
+
DeviceInfo: {
|
|
81
|
+
getConstants: () => ({
|
|
82
|
+
Dimensions: {
|
|
83
|
+
window: { width: 375, height: 667, scale: 2, fontScale: 1 },
|
|
84
|
+
screen: { width: 375, height: 667, scale: 2, fontScale: 1 },
|
|
85
|
+
},
|
|
86
|
+
}),
|
|
87
|
+
},
|
|
88
|
+
};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Jest Pre-Setup File
|
|
3
|
+
*
|
|
4
|
+
* Runs before Jest loads any other modules to set up global flags needed
|
|
5
|
+
* by React Native. This file must be JavaScript (not TypeScript) and must
|
|
6
|
+
* not import any modules that depend on the global flags being set.
|
|
7
|
+
*
|
|
8
|
+
* @remarks
|
|
9
|
+
* - Listed in `setupFiles` (runs before test framework loads)
|
|
10
|
+
* - Sets `__DEV__`, `IS_REACT_ACT_ENVIRONMENT`, and other RN globals
|
|
11
|
+
* - Stubs `__turboModuleProxy` so native module requires don't throw
|
|
12
|
+
* - React 19 throws null on cleanup — the unhandledRejection handler
|
|
13
|
+
* suppresses those while still surfacing real errors
|
|
14
|
+
* @see https://github.com/expo/expo/issues/38046 React 19 cleanup issue
|
|
15
|
+
* @see https://github.com/expo/expo/issues/40184 Jest 30 + expo-router
|
|
16
|
+
* @module jest.setup.pre
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
// Polyfill structuredClone for jsdom environment
|
|
20
|
+
// structuredClone is available in Node.js 17+ but jsdom doesn't expose it
|
|
21
|
+
if (typeof global.structuredClone === "undefined") {
|
|
22
|
+
global.structuredClone = val => JSON.parse(JSON.stringify(val));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Handle unhandled promise rejections BEFORE any other code runs
|
|
26
|
+
// This must be set up first to catch React 19 "thrown: null" issues
|
|
27
|
+
// Store the handler so it can be removed after tests complete (in jest.setup.ts)
|
|
28
|
+
global.__unhandledRejectionHandler = (reason, _promise) => {
|
|
29
|
+
// Silently ignore null rejections from React 19 cleanup
|
|
30
|
+
if (reason === null) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
// Re-throw other unhandled rejections so tests still fail for real issues
|
|
34
|
+
throw reason;
|
|
35
|
+
};
|
|
36
|
+
process.on("unhandledRejection", global.__unhandledRejectionHandler);
|
|
37
|
+
|
|
38
|
+
// Disable RTLRN's auto cleanup to avoid "thrown: null" errors from React 19
|
|
39
|
+
// We handle cleanup manually in jest.setup.ts
|
|
40
|
+
process.env.RNTL_SKIP_AUTO_CLEANUP = "true";
|
|
41
|
+
|
|
42
|
+
// Set React Native test environment flags BEFORE any modules are loaded
|
|
43
|
+
global.__DEV__ = true;
|
|
44
|
+
global.IS_REACT_ACT_ENVIRONMENT = true;
|
|
45
|
+
global.IS_REACT_NATIVE_TEST_ENVIRONMENT = true;
|
|
46
|
+
|
|
47
|
+
// Mock React Native bridge for testing
|
|
48
|
+
global.__fbBatchedBridgeConfig = {
|
|
49
|
+
remoteModuleConfig: [],
|
|
50
|
+
localModulesConfig: [],
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// Mock TurboModuleRegistry using external mock configuration
|
|
54
|
+
const mockModules = require("./jest.config.react-native-mock");
|
|
55
|
+
|
|
56
|
+
global.__turboModuleProxy = function (moduleName) {
|
|
57
|
+
return mockModules[moduleName] || null;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// Ensure global timers are available (required by @testing-library/react-native)
|
|
61
|
+
// Jest's environment should have these, but we ensure they're set
|
|
62
|
+
if (typeof global.setTimeout === "undefined") {
|
|
63
|
+
global.setTimeout = globalThis.setTimeout;
|
|
64
|
+
}
|
|
65
|
+
if (typeof global.clearTimeout === "undefined") {
|
|
66
|
+
global.clearTimeout = globalThis.clearTimeout;
|
|
67
|
+
}
|
|
68
|
+
if (typeof global.setInterval === "undefined") {
|
|
69
|
+
global.setInterval = globalThis.setInterval;
|
|
70
|
+
}
|
|
71
|
+
if (typeof global.clearInterval === "undefined") {
|
|
72
|
+
global.clearInterval = globalThis.clearInterval;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Mock window object for web-specific code in platform-agnostic tests
|
|
76
|
+
// This allows tests that check Platform.OS === 'web' branches to work
|
|
77
|
+
// Important: Include timer functions because RTLRN checks typeof window !== 'undefined'
|
|
78
|
+
global.window = {
|
|
79
|
+
// Timer functions (required by RTLRN)
|
|
80
|
+
setTimeout: global.setTimeout,
|
|
81
|
+
clearTimeout: global.clearTimeout,
|
|
82
|
+
setInterval: global.setInterval,
|
|
83
|
+
clearInterval: global.clearInterval,
|
|
84
|
+
setImmediate: global.setImmediate,
|
|
85
|
+
// Web-specific mocks
|
|
86
|
+
confirm: jest.fn(() => true),
|
|
87
|
+
alert: jest.fn(),
|
|
88
|
+
open: jest.fn(),
|
|
89
|
+
location: {
|
|
90
|
+
href: "",
|
|
91
|
+
origin: "http://localhost",
|
|
92
|
+
pathname: "/",
|
|
93
|
+
search: "",
|
|
94
|
+
hash: "",
|
|
95
|
+
},
|
|
96
|
+
addEventListener: jest.fn(),
|
|
97
|
+
removeEventListener: jest.fn(),
|
|
98
|
+
navigator: {
|
|
99
|
+
userAgent: "jest-test",
|
|
100
|
+
},
|
|
101
|
+
matchMedia: jest.fn(() => ({
|
|
102
|
+
matches: false,
|
|
103
|
+
addListener: jest.fn(),
|
|
104
|
+
removeListener: jest.fn(),
|
|
105
|
+
})),
|
|
106
|
+
};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Jest Setup File
|
|
3
|
+
*
|
|
4
|
+
* Configures the testing environment after Jest loads modules.
|
|
5
|
+
* Sets up React Native Testing Library matchers, global mocks,
|
|
6
|
+
* and manual cleanup for React 19 compatibility.
|
|
7
|
+
*
|
|
8
|
+
* @remarks
|
|
9
|
+
* - Listed in `setupFilesAfterEnv` (runs after test framework loads)
|
|
10
|
+
* - Basic globals like `__DEV__` are set earlier in `jest.setup.pre.js`
|
|
11
|
+
* - Manual cleanup replaces RTLRN auto-cleanup to handle React 19's
|
|
12
|
+
* null-throw behavior during component teardown
|
|
13
|
+
* @see https://github.com/expo/expo/issues/38046 React 19 cleanup issue
|
|
14
|
+
* @see https://github.com/expo/expo/issues/40184 Jest 30 + expo-router
|
|
15
|
+
* @see {@link https://callstack.github.io/react-native-testing-library/ | RTLRN Docs}
|
|
16
|
+
* @module jest.setup
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
// Import from pure to avoid auto-cleanup (we set RNTL_SKIP_AUTO_CLEANUP in pre-setup)
|
|
20
|
+
// Then extend Jest matchers with React Native Testing Library matchers
|
|
21
|
+
import { cleanup } from "@testing-library/react-native/pure";
|
|
22
|
+
import "@testing-library/react-native/build/matchers/extend-expect";
|
|
23
|
+
|
|
24
|
+
// Mock environment variables module for type-safe env access in tests
|
|
25
|
+
// Tests can override specific values via jest.spyOn or jest.doMock
|
|
26
|
+
// Replace the mock below with your project's actual env module and values
|
|
27
|
+
jest.mock("@/lib/env", () => ({
|
|
28
|
+
env: {
|
|
29
|
+
EXPO_PUBLIC_ENV: "dev",
|
|
30
|
+
// Add your project-specific environment variables here
|
|
31
|
+
},
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
// Extend global type for the unhandledRejection handler set in jest.setup.pre.js
|
|
35
|
+
declare global {
|
|
36
|
+
let __unhandledRejectionHandler:
|
|
37
|
+
| ((reason: unknown, promise: Promise<unknown>) => void)
|
|
38
|
+
| undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Manual cleanup after each test (replacing RTLRN's auto cleanup)
|
|
42
|
+
// This handles React 19's cleanup which can throw null
|
|
43
|
+
afterEach(async () => {
|
|
44
|
+
try {
|
|
45
|
+
await cleanup();
|
|
46
|
+
} catch (error) {
|
|
47
|
+
// Silently ignore null throws from React 19 cleanup
|
|
48
|
+
if (error !== null) {
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Mock ResizeObserver for tests that use it
|
|
55
|
+
// eslint-disable-next-line functional/no-classes -- Mock implementation required for testing
|
|
56
|
+
global.ResizeObserver = class ResizeObserver {
|
|
57
|
+
/** Starts observing an element — no-op for tests */
|
|
58
|
+
observe(): void {}
|
|
59
|
+
/** Stops observing an element — no-op for tests */
|
|
60
|
+
unobserve(): void {}
|
|
61
|
+
/** Disconnects observer — no-op for tests */
|
|
62
|
+
disconnect(): void {}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// Mock expo-router for tests using navigation hooks
|
|
66
|
+
jest.mock("expo-router", () => ({
|
|
67
|
+
useRouter: jest.fn(() => ({
|
|
68
|
+
push: jest.fn(),
|
|
69
|
+
replace: jest.fn(),
|
|
70
|
+
back: jest.fn(),
|
|
71
|
+
canGoBack: jest.fn(() => true),
|
|
72
|
+
})),
|
|
73
|
+
usePathname: jest.fn(() => "/"),
|
|
74
|
+
useLocalSearchParams: jest.fn(() => ({})),
|
|
75
|
+
useGlobalSearchParams: jest.fn(() => ({})),
|
|
76
|
+
useSegments: jest.fn(() => []),
|
|
77
|
+
useNavigation: jest.fn(() => ({
|
|
78
|
+
navigate: jest.fn(),
|
|
79
|
+
goBack: jest.fn(),
|
|
80
|
+
getParent: jest.fn(() => ({
|
|
81
|
+
setOptions: jest.fn(),
|
|
82
|
+
})),
|
|
83
|
+
})),
|
|
84
|
+
Link: ({ children }: { children: React.ReactNode }) => children,
|
|
85
|
+
Stack: {
|
|
86
|
+
Screen: () => null,
|
|
87
|
+
},
|
|
88
|
+
Tabs: {
|
|
89
|
+
Screen: () => null,
|
|
90
|
+
},
|
|
91
|
+
}));
|
|
92
|
+
|
|
93
|
+
// Mock @react-native-firebase/analytics
|
|
94
|
+
jest.mock("@react-native-firebase/analytics", () => {
|
|
95
|
+
const logEvent = jest.fn();
|
|
96
|
+
const getAnalytics = jest.fn(() => ({}));
|
|
97
|
+
return { logEvent, getAnalytics };
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// Mock firebase/analytics
|
|
101
|
+
jest.mock("firebase/analytics", () => {
|
|
102
|
+
const logEvent = jest.fn();
|
|
103
|
+
const getAnalytics = jest.fn(() => ({}));
|
|
104
|
+
return { logEvent, getAnalytics };
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// Clear mocks after each test
|
|
108
|
+
afterEach(() => {
|
|
109
|
+
jest.clearAllMocks();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// Remove the unhandledRejection handler after all tests to allow Jest to exit cleanly
|
|
113
|
+
// The handler is registered in jest.setup.pre.js and stored on global
|
|
114
|
+
afterAll(() => {
|
|
115
|
+
if (__unhandledRejectionHandler) {
|
|
116
|
+
process.removeListener("unhandledRejection", __unhandledRejectionHandler);
|
|
117
|
+
}
|
|
118
|
+
});
|
package/package.json
CHANGED
|
@@ -85,7 +85,7 @@
|
|
|
85
85
|
},
|
|
86
86
|
"resolutions": {},
|
|
87
87
|
"name": "@codyswann/lisa",
|
|
88
|
-
"version": "1.
|
|
88
|
+
"version": "1.16.0",
|
|
89
89
|
"description": "Claude Code governance framework that applies guardrails, guidance, and automated enforcement to projects",
|
|
90
90
|
"main": "dist/index.js",
|
|
91
91
|
"bin": {
|
|
@@ -79,7 +79,11 @@ elif [ "$PACKAGE_MANAGER" = "bun" ]; then
|
|
|
79
79
|
# Excluding GHSA-8qq5-rm4j-mr97: node-tar path sanitization vulnerability
|
|
80
80
|
# Nested dependency in @expo/cli - bun resolves to patched version but audit still flags it
|
|
81
81
|
# Risk: Low - only affects tar extraction with malicious filenames, not our use case
|
|
82
|
-
|
|
82
|
+
|
|
83
|
+
# Excluding GHSA-37qj-frw5-hhjh: fast-xml-parser RangeError DoS with numeric entities
|
|
84
|
+
# Transitive dependency via @react-native-community/cli (Android/iOS build tooling)
|
|
85
|
+
# Risk: None - CLI build tool, not a production runtime dependency
|
|
86
|
+
if ! bun audit --audit-level=high --ignore GHSA-5j98-mcp5-4vw2 --ignore GHSA-8qq5-rm4j-mr97 --ignore GHSA-37qj-frw5-hhjh; then
|
|
83
87
|
echo "⚠️ Security audit failed. Please fix high/critical vulnerabilities before pushing."
|
|
84
88
|
exit 1
|
|
85
89
|
fi
|
|
@@ -109,7 +109,7 @@ export const getBaseConfigs = () => [
|
|
|
109
109
|
},
|
|
110
110
|
|
|
111
111
|
// Code quality
|
|
112
|
-
sonarjs.configs.recommended,
|
|
112
|
+
...(sonarjs.configs?.recommended ? [sonarjs.configs.recommended] : []),
|
|
113
113
|
{
|
|
114
114
|
plugins: {
|
|
115
115
|
"@eslint-community/eslint-comments": eslintComments,
|