@base44-preview/vite-plugin 0.1.0 → 0.1.1-dev.c4331fa
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/compat/agents.cjs +13 -0
- package/compat/entities.cjs +16 -2
- package/dist/ErrorOverlay.d.ts +12 -0
- package/dist/ErrorOverlay.d.ts.map +1 -0
- package/dist/ErrorOverlay.js +51 -0
- package/dist/ErrorOverlay.js.map +1 -0
- package/dist/error-overlay-plugin.d.ts +3 -0
- package/dist/error-overlay-plugin.d.ts.map +1 -0
- package/dist/error-overlay-plugin.js +15 -0
- package/dist/error-overlay-plugin.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +105 -0
- package/dist/index.js.map +1 -0
- package/dist/visual-edit-plugin.d.ts +3 -0
- package/dist/visual-edit-plugin.d.ts.map +1 -0
- package/dist/visual-edit-plugin.js +229 -0
- package/dist/visual-edit-plugin.js.map +1 -0
- package/package.json +16 -2
- package/src/ErrorOverlay.ts +71 -0
- package/src/error-overlay-plugin.ts +19 -0
- package/src/index.ts +129 -72
- package/src/visual-edit-plugin.ts +268 -0
- package/.github/workflows/claude-code-review.yml +0 -78
- package/.github/workflows/claude.yml +0 -63
- package/.github/workflows/main-publish.yml +0 -119
- package/.github/workflows/manual-publish.yml +0 -127
- package/.github/workflows/preview-publish.yml +0 -210
- package/.github/workflows/unit-tests.yml +0 -27
- package/tsconfig.json +0 -36
package/src/index.ts
CHANGED
|
@@ -1,82 +1,139 @@
|
|
|
1
|
-
import type { Plugin } from "vite";
|
|
1
|
+
import type { Plugin, UserConfig } from "vite";
|
|
2
2
|
import { loadEnv } from "vite";
|
|
3
|
+
import { errorOverlayPlugin } from "./error-overlay-plugin.js";
|
|
4
|
+
import { visualEditPlugin } from "./visual-edit-plugin.js";
|
|
3
5
|
|
|
4
|
-
|
|
5
|
-
return {
|
|
6
|
-
name: "base44",
|
|
7
|
-
config: ({ mode }) => {
|
|
8
|
-
const env = loadEnv(mode ?? "development", process.cwd(), "");
|
|
6
|
+
const isRunningInSandbox = !!process.env.MODAL_SANDBOX_ID;
|
|
9
7
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
protocol: "wss",
|
|
29
|
-
clientPort: 443,
|
|
8
|
+
export default function vitePlugin(
|
|
9
|
+
opts: {
|
|
10
|
+
legacySDKImports?: boolean;
|
|
11
|
+
} = {}
|
|
12
|
+
) {
|
|
13
|
+
const { legacySDKImports = false } = opts;
|
|
14
|
+
|
|
15
|
+
return [
|
|
16
|
+
{
|
|
17
|
+
name: "base44",
|
|
18
|
+
config: ({ mode }) => {
|
|
19
|
+
const env = loadEnv(mode ?? "development", process.cwd(), "");
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
resolve: {
|
|
23
|
+
alias: {
|
|
24
|
+
"@/": "/src/",
|
|
25
|
+
},
|
|
30
26
|
},
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
27
|
+
...(isRunningInSandbox
|
|
28
|
+
? ({
|
|
29
|
+
server: {
|
|
30
|
+
host: "0.0.0.0", // Bind to all interfaces for container access
|
|
31
|
+
port: 5173,
|
|
32
|
+
strictPort: true,
|
|
33
|
+
// Allow all hosts - essential for Modal tunnel URLs
|
|
34
|
+
allowedHosts: true,
|
|
35
|
+
watch: {
|
|
36
|
+
// Enable polling for better file change detection in containers
|
|
37
|
+
usePolling: true,
|
|
38
|
+
interval: 100, // Check every 100ms for responsive HMR
|
|
39
|
+
},
|
|
40
|
+
hmr: {
|
|
41
|
+
protocol: "wss",
|
|
42
|
+
clientPort: 443,
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
build: {
|
|
46
|
+
rollupOptions: {
|
|
47
|
+
onwarn(warning, warn) {
|
|
48
|
+
// Treat import errors as fatal errors
|
|
49
|
+
if (
|
|
50
|
+
warning.code === "UNRESOLVED_IMPORT" ||
|
|
51
|
+
warning.code === "MISSING_EXPORT"
|
|
52
|
+
) {
|
|
53
|
+
throw new Error(`Build failed: ${warning.message}`);
|
|
54
|
+
}
|
|
55
|
+
// Use default for other warnings
|
|
56
|
+
warn(warning);
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
} as Partial<UserConfig>)
|
|
61
|
+
: {}),
|
|
62
|
+
optimizeDeps: {
|
|
63
|
+
esbuildOptions: {
|
|
64
|
+
loader: {
|
|
65
|
+
".js": "jsx",
|
|
66
|
+
},
|
|
67
|
+
...(legacySDKImports
|
|
68
|
+
? {
|
|
69
|
+
define: {
|
|
70
|
+
"process.env.VITE_BASE44_APP_ID": JSON.stringify(
|
|
71
|
+
env.VITE_BASE44_APP_ID
|
|
72
|
+
),
|
|
73
|
+
"process.env.VITE_BASE44_BACKEND_URL": JSON.stringify(
|
|
74
|
+
env.VITE_BASE44_BACKEND_URL
|
|
75
|
+
),
|
|
76
|
+
},
|
|
77
|
+
}
|
|
78
|
+
: {}),
|
|
41
79
|
},
|
|
42
80
|
},
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
81
|
+
};
|
|
82
|
+
},
|
|
83
|
+
resolveId(source, importer, options) {
|
|
84
|
+
if (legacySDKImports) {
|
|
85
|
+
if (source.includes("/entities")) {
|
|
86
|
+
return this.resolve(
|
|
87
|
+
"@base44/vite-plugin/compat/entities.cjs",
|
|
88
|
+
importer,
|
|
89
|
+
options
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (source.includes("/functions")) {
|
|
94
|
+
return this.resolve(
|
|
95
|
+
"@base44/vite-plugin/compat/functions.cjs",
|
|
96
|
+
importer,
|
|
97
|
+
options
|
|
98
|
+
);
|
|
99
|
+
}
|
|
62
100
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
101
|
+
if (source.includes("/integrations")) {
|
|
102
|
+
return this.resolve(
|
|
103
|
+
"@base44/vite-plugin/compat/integrations.cjs",
|
|
104
|
+
importer,
|
|
105
|
+
options
|
|
106
|
+
);
|
|
107
|
+
}
|
|
70
108
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
109
|
+
if (source.includes("/agents")) {
|
|
110
|
+
return this.resolve(
|
|
111
|
+
"@base44/vite-plugin/compat/agents.cjs",
|
|
112
|
+
importer,
|
|
113
|
+
options
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
78
117
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
118
|
+
return null;
|
|
119
|
+
},
|
|
120
|
+
} as Plugin,
|
|
121
|
+
...(isRunningInSandbox
|
|
122
|
+
? [
|
|
123
|
+
{
|
|
124
|
+
name: "iframe-hmr",
|
|
125
|
+
configureServer(server) {
|
|
126
|
+
server.middlewares.use((req, res, next) => {
|
|
127
|
+
// Allow iframe embedding
|
|
128
|
+
res.setHeader("X-Frame-Options", "ALLOWALL");
|
|
129
|
+
res.setHeader("Content-Security-Policy", "frame-ancestors *;");
|
|
130
|
+
next();
|
|
131
|
+
});
|
|
132
|
+
},
|
|
133
|
+
} as Plugin,
|
|
134
|
+
errorOverlayPlugin(),
|
|
135
|
+
visualEditPlugin(),
|
|
136
|
+
]
|
|
137
|
+
: []),
|
|
138
|
+
];
|
|
82
139
|
}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import { parse } from "@babel/parser";
|
|
2
|
+
import { default as traverse } from "@babel/traverse";
|
|
3
|
+
import { default as generate } from "@babel/generator";
|
|
4
|
+
import * as t from "@babel/types";
|
|
5
|
+
import type { Plugin } from "vite";
|
|
6
|
+
|
|
7
|
+
// Helper function to check if JSX element contains dynamic content
|
|
8
|
+
function checkIfElementHasDynamicContent(jsxElement: any) {
|
|
9
|
+
let hasDynamicContent = false;
|
|
10
|
+
|
|
11
|
+
// Helper function to check if any node contains dynamic patterns
|
|
12
|
+
function checkNodeForDynamicContent(node: any) {
|
|
13
|
+
// JSX expressions like {variable}, {func()}, {obj.prop}
|
|
14
|
+
if (t.isJSXExpressionContainer(node)) {
|
|
15
|
+
const expression = node.expression;
|
|
16
|
+
|
|
17
|
+
// Skip empty expressions {}
|
|
18
|
+
if (t.isJSXEmptyExpression(expression)) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Any non-literal expression is considered dynamic
|
|
23
|
+
if (!t.isLiteral(expression)) {
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Template literals with expressions `Hello ${name}`
|
|
29
|
+
if (t.isTemplateLiteral(node) && node.expressions.length > 0) {
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Member expressions like props.title, state.value
|
|
34
|
+
if (t.isMemberExpression(node)) {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Function calls like getData(), format()
|
|
39
|
+
if (t.isCallExpression(node)) {
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Conditional expressions like condition ? "yes" : "no"
|
|
44
|
+
if (t.isConditionalExpression(node)) {
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Identifier references (could be props, state, variables)
|
|
49
|
+
if (t.isIdentifier(node)) {
|
|
50
|
+
// Common dynamic identifiers
|
|
51
|
+
const dynamicNames = [
|
|
52
|
+
"props",
|
|
53
|
+
"state",
|
|
54
|
+
"data",
|
|
55
|
+
"item",
|
|
56
|
+
"value",
|
|
57
|
+
"text",
|
|
58
|
+
"content",
|
|
59
|
+
];
|
|
60
|
+
if (dynamicNames.some((name) => node.name.includes(name))) {
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Recursively traverse all child nodes
|
|
69
|
+
function traverseNode(node: any) {
|
|
70
|
+
if (checkNodeForDynamicContent(node)) {
|
|
71
|
+
hasDynamicContent = true;
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Recursively check child nodes
|
|
76
|
+
Object.keys(node).forEach((key) => {
|
|
77
|
+
const value = node[key];
|
|
78
|
+
|
|
79
|
+
if (Array.isArray(value)) {
|
|
80
|
+
value.forEach((child) => {
|
|
81
|
+
if (child && typeof child === "object" && child.type) {
|
|
82
|
+
traverseNode(child);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
} else if (value && typeof value === "object" && value.type) {
|
|
86
|
+
traverseNode(value);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Check all children of the JSX element
|
|
92
|
+
jsxElement.children.forEach((child: any) => {
|
|
93
|
+
if (hasDynamicContent) return; // Early exit if already found dynamic content
|
|
94
|
+
traverseNode(child);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
return hasDynamicContent;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function visualEditPlugin() {
|
|
101
|
+
return {
|
|
102
|
+
name: "visual-edit-transform",
|
|
103
|
+
apply: (config) => config.mode === "development",
|
|
104
|
+
enforce: "pre",
|
|
105
|
+
order: "pre",
|
|
106
|
+
// Inject Tailwind CDN for visual editing capabilities
|
|
107
|
+
transformIndexHtml(html: any) {
|
|
108
|
+
// Inject the Tailwind CSS CDN script right before the closing </head> tag
|
|
109
|
+
const tailwindScript = ` <!-- Tailwind CSS CDN for visual editing -->\n <script src="https://cdn.tailwindcss.com"></script>\n `;
|
|
110
|
+
return html.replace("</head>", tailwindScript + "</head>");
|
|
111
|
+
},
|
|
112
|
+
transform(code: any, id: any) {
|
|
113
|
+
// Skip node_modules and visual-edit-agent itself
|
|
114
|
+
if (id.includes("node_modules") || id.includes("visual-edit-agent")) {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Process JS/JSX/TS/TSX files
|
|
119
|
+
if (!id.match(/\.(jsx?|tsx?)$/)) {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Extract filename from path, preserving pages/ or components/ structure
|
|
124
|
+
const pathParts = id.split("/");
|
|
125
|
+
let filename;
|
|
126
|
+
|
|
127
|
+
// Check if this is a pages or components file
|
|
128
|
+
if (id.includes("/pages/")) {
|
|
129
|
+
const pagesIndex = pathParts.findIndex((part: any) => part === "pages");
|
|
130
|
+
if (pagesIndex >= 0 && pagesIndex < pathParts.length - 1) {
|
|
131
|
+
// Get all parts from 'pages' to the file, preserving nested structure
|
|
132
|
+
const relevantParts = pathParts.slice(pagesIndex, pathParts.length);
|
|
133
|
+
const lastPart = relevantParts[relevantParts.length - 1];
|
|
134
|
+
// Remove file extension from the last part
|
|
135
|
+
relevantParts[relevantParts.length - 1] = lastPart.includes(".")
|
|
136
|
+
? lastPart.split(".")[0]
|
|
137
|
+
: lastPart;
|
|
138
|
+
filename = relevantParts.join("/");
|
|
139
|
+
} else {
|
|
140
|
+
filename = pathParts[pathParts.length - 1];
|
|
141
|
+
if (filename.includes(".")) {
|
|
142
|
+
filename = filename.split(".")[0];
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
} else if (id.includes("/components/")) {
|
|
146
|
+
const componentsIndex = pathParts.findIndex(
|
|
147
|
+
(part: any) => part === "components"
|
|
148
|
+
);
|
|
149
|
+
if (componentsIndex >= 0 && componentsIndex < pathParts.length - 1) {
|
|
150
|
+
// Get all parts from 'components' to the file, preserving nested structure
|
|
151
|
+
const relevantParts = pathParts.slice(
|
|
152
|
+
componentsIndex,
|
|
153
|
+
pathParts.length
|
|
154
|
+
);
|
|
155
|
+
const lastPart = relevantParts[relevantParts.length - 1];
|
|
156
|
+
// Remove file extension from the last part
|
|
157
|
+
relevantParts[relevantParts.length - 1] = lastPart.includes(".")
|
|
158
|
+
? lastPart.split(".")[0]
|
|
159
|
+
: lastPart;
|
|
160
|
+
filename = relevantParts.join("/");
|
|
161
|
+
} else {
|
|
162
|
+
filename = pathParts[pathParts.length - 1];
|
|
163
|
+
if (filename.includes(".")) {
|
|
164
|
+
filename = filename.split(".")[0];
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
} else {
|
|
168
|
+
// For other files (like layout), just use the filename
|
|
169
|
+
filename = pathParts[pathParts.length - 1];
|
|
170
|
+
if (filename.includes(".")) {
|
|
171
|
+
filename = filename.split(".")[0];
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
try {
|
|
176
|
+
// Parse the code into an AST
|
|
177
|
+
const ast = parse(code, {
|
|
178
|
+
sourceType: "module",
|
|
179
|
+
plugins: [
|
|
180
|
+
"jsx",
|
|
181
|
+
"typescript",
|
|
182
|
+
"decorators-legacy",
|
|
183
|
+
"classProperties",
|
|
184
|
+
"objectRestSpread",
|
|
185
|
+
"functionBind",
|
|
186
|
+
"exportDefaultFrom",
|
|
187
|
+
"exportNamespaceFrom",
|
|
188
|
+
"dynamicImport",
|
|
189
|
+
"nullishCoalescingOperator",
|
|
190
|
+
"optionalChaining",
|
|
191
|
+
"asyncGenerators",
|
|
192
|
+
"bigInt",
|
|
193
|
+
"optionalCatchBinding",
|
|
194
|
+
"throwExpressions",
|
|
195
|
+
],
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// Traverse the AST and add source location and dynamic content attributes to JSX elements
|
|
199
|
+
let elementsProcessed = 0;
|
|
200
|
+
traverse.default(ast, {
|
|
201
|
+
JSXElement(path) {
|
|
202
|
+
const jsxElement = path.node;
|
|
203
|
+
const openingElement = jsxElement.openingElement;
|
|
204
|
+
|
|
205
|
+
// Skip fragments
|
|
206
|
+
if (t.isJSXFragment(jsxElement)) return;
|
|
207
|
+
|
|
208
|
+
// Skip if already has source location attribute
|
|
209
|
+
const hasSourceLocation = openingElement.attributes.some(
|
|
210
|
+
(attr) =>
|
|
211
|
+
t.isJSXAttribute(attr) &&
|
|
212
|
+
t.isJSXIdentifier(attr.name) &&
|
|
213
|
+
attr.name.name === "data-source-location"
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
if (hasSourceLocation) return;
|
|
217
|
+
|
|
218
|
+
// Get line and column from AST node location
|
|
219
|
+
const { line, column } = openingElement.loc?.start || {
|
|
220
|
+
line: 1,
|
|
221
|
+
column: 0,
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
// Create the source location attribute
|
|
225
|
+
const sourceLocationAttr = t.jsxAttribute(
|
|
226
|
+
t.jsxIdentifier("data-source-location"),
|
|
227
|
+
t.stringLiteral(`${filename}:${line}:${column}`)
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
// Check if element has dynamic content
|
|
231
|
+
const isDynamic = checkIfElementHasDynamicContent(jsxElement);
|
|
232
|
+
|
|
233
|
+
// Create the dynamic content attribute
|
|
234
|
+
const dynamicContentAttr = t.jsxAttribute(
|
|
235
|
+
t.jsxIdentifier("data-dynamic-content"),
|
|
236
|
+
t.stringLiteral(isDynamic ? "true" : "false")
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
// Add both attributes to the beginning of the attributes array
|
|
240
|
+
openingElement.attributes.unshift(
|
|
241
|
+
sourceLocationAttr,
|
|
242
|
+
dynamicContentAttr
|
|
243
|
+
);
|
|
244
|
+
elementsProcessed++;
|
|
245
|
+
},
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// Generate the code back from the AST
|
|
249
|
+
const result = generate.default(ast, {
|
|
250
|
+
compact: false,
|
|
251
|
+
concise: false,
|
|
252
|
+
retainLines: true,
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
code: result.code,
|
|
257
|
+
map: null,
|
|
258
|
+
};
|
|
259
|
+
} catch (error) {
|
|
260
|
+
console.error("Failed to add source location to JSX:", error);
|
|
261
|
+
return {
|
|
262
|
+
code: code, // Return original code on failure
|
|
263
|
+
map: null,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
},
|
|
267
|
+
} as Plugin;
|
|
268
|
+
}
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
name: Claude Code Review
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
pull_request:
|
|
5
|
-
types: [opened, synchronize]
|
|
6
|
-
# Optional: Only run on specific file changes
|
|
7
|
-
# paths:
|
|
8
|
-
# - "src/**/*.ts"
|
|
9
|
-
# - "src/**/*.tsx"
|
|
10
|
-
# - "src/**/*.js"
|
|
11
|
-
# - "src/**/*.jsx"
|
|
12
|
-
|
|
13
|
-
jobs:
|
|
14
|
-
claude-review:
|
|
15
|
-
if: |
|
|
16
|
-
github.actor != 'github-actions[bot]' &&
|
|
17
|
-
github.actor != 'claude-code[bot]' &&
|
|
18
|
-
github.actor != 'claude[bot]' &&
|
|
19
|
-
github.actor != 'claude' &&
|
|
20
|
-
github.event.pull_request.head.commit.parents[1] == null
|
|
21
|
-
|
|
22
|
-
runs-on: ubuntu-latest
|
|
23
|
-
permissions:
|
|
24
|
-
contents: read
|
|
25
|
-
pull-requests: read
|
|
26
|
-
issues: read
|
|
27
|
-
id-token: write
|
|
28
|
-
|
|
29
|
-
steps:
|
|
30
|
-
- name: Checkout repository
|
|
31
|
-
uses: actions/checkout@v4
|
|
32
|
-
with:
|
|
33
|
-
fetch-depth: 1
|
|
34
|
-
|
|
35
|
-
- name: Run Claude Code Review
|
|
36
|
-
id: claude-review
|
|
37
|
-
uses: anthropics/claude-code-action@beta
|
|
38
|
-
with:
|
|
39
|
-
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
40
|
-
|
|
41
|
-
# Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4)
|
|
42
|
-
model: "claude-opus-4-20250514"
|
|
43
|
-
|
|
44
|
-
# Direct prompt for automated review (no @claude mention needed)
|
|
45
|
-
direct_prompt: |
|
|
46
|
-
Please review this pull request and provide feedback on:
|
|
47
|
-
- Code quality and best practices
|
|
48
|
-
- Potential bugs or issues
|
|
49
|
-
- Performance considerations
|
|
50
|
-
- Security concerns
|
|
51
|
-
- Test coverage
|
|
52
|
-
|
|
53
|
-
Be constructive and helpful in your feedback.
|
|
54
|
-
|
|
55
|
-
# Optional: Use sticky comments to make Claude reuse the same comment on subsequent pushes to the same PR
|
|
56
|
-
# use_sticky_comment: true
|
|
57
|
-
|
|
58
|
-
# Optional: Customize review based on file types
|
|
59
|
-
# direct_prompt: |
|
|
60
|
-
# Review this PR focusing on:
|
|
61
|
-
# - For TypeScript files: Type safety and proper interface usage
|
|
62
|
-
# - For API endpoints: Security, input validation, and error handling
|
|
63
|
-
# - For React components: Performance, accessibility, and best practices
|
|
64
|
-
# - For tests: Coverage, edge cases, and test quality
|
|
65
|
-
|
|
66
|
-
# Optional: Different prompts for different authors
|
|
67
|
-
# direct_prompt: |
|
|
68
|
-
# ${{ github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' &&
|
|
69
|
-
# 'Welcome! Please review this PR from a first-time contributor. Be encouraging and provide detailed explanations for any suggestions.' ||
|
|
70
|
-
# 'Please provide a thorough code review focusing on our coding standards and best practices.' }}
|
|
71
|
-
|
|
72
|
-
# Optional: Add specific tools for running tests or linting
|
|
73
|
-
# allowed_tools: "Bash(npm run test),Bash(npm run lint),Bash(npm run typecheck)"
|
|
74
|
-
|
|
75
|
-
# Optional: Skip review for certain conditions
|
|
76
|
-
# if: |
|
|
77
|
-
# !contains(github.event.pull_request.title, '[skip-review]') &&
|
|
78
|
-
# !contains(github.event.pull_request.title, '[WIP]')
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
name: Claude Code
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
issue_comment:
|
|
5
|
-
types: [created]
|
|
6
|
-
pull_request_review_comment:
|
|
7
|
-
types: [created]
|
|
8
|
-
issues:
|
|
9
|
-
types: [opened, assigned]
|
|
10
|
-
pull_request_review:
|
|
11
|
-
types: [submitted]
|
|
12
|
-
|
|
13
|
-
jobs:
|
|
14
|
-
claude:
|
|
15
|
-
if: |
|
|
16
|
-
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
|
|
17
|
-
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
|
|
18
|
-
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
|
|
19
|
-
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
|
|
20
|
-
runs-on: ubuntu-latest
|
|
21
|
-
permissions:
|
|
22
|
-
contents: read
|
|
23
|
-
pull-requests: read
|
|
24
|
-
issues: read
|
|
25
|
-
id-token: write
|
|
26
|
-
actions: read # Required for Claude to read CI results on PRs
|
|
27
|
-
steps:
|
|
28
|
-
- name: Checkout repository
|
|
29
|
-
uses: actions/checkout@v4
|
|
30
|
-
with:
|
|
31
|
-
fetch-depth: 1
|
|
32
|
-
|
|
33
|
-
- name: Run Claude Code
|
|
34
|
-
id: claude
|
|
35
|
-
uses: anthropics/claude-code-action@beta
|
|
36
|
-
with:
|
|
37
|
-
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
38
|
-
|
|
39
|
-
# This is an optional setting that allows Claude to read CI results on PRs
|
|
40
|
-
additional_permissions: |
|
|
41
|
-
actions: read
|
|
42
|
-
|
|
43
|
-
# Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4)
|
|
44
|
-
model: "claude-opus-4-20250514"
|
|
45
|
-
|
|
46
|
-
# Optional: Customize the trigger phrase (default: @claude)
|
|
47
|
-
# trigger_phrase: "/claude"
|
|
48
|
-
|
|
49
|
-
# Optional: Trigger when specific user is assigned to an issue
|
|
50
|
-
# assignee_trigger: "claude-bot"
|
|
51
|
-
|
|
52
|
-
# Optional: Allow Claude to run specific commands
|
|
53
|
-
allowed_tools: "Bash(npm install),Bash(npm run build),Bash(npm run test:*),Bash(npm run lint:*)"
|
|
54
|
-
|
|
55
|
-
# Optional: Add custom instructions for Claude to customize its behavior for your project
|
|
56
|
-
# custom_instructions: |
|
|
57
|
-
# Follow our coding standards
|
|
58
|
-
# Ensure all new code has tests
|
|
59
|
-
# Use TypeScript for new files
|
|
60
|
-
|
|
61
|
-
# Optional: Custom environment variables for Claude
|
|
62
|
-
# claude_env: |
|
|
63
|
-
# NODE_ENV: test
|