pullsmith 0.0.1 → 0.0.2
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/bin/pullsmith.js +22 -1
- package/package.json +7 -2
- package/src/commands/doctor.js +50 -0
- package/src/commands/init.js +202 -0
- package/src/commands/validate.js +47 -0
- package/src/lib/auth.js +25 -0
- package/src/lib/config.js +1 -0
- package/src/lib/git.js +16 -0
- package/src/utils/extract/agent/model.sh +17 -0
- package/src/utils/extract/agent/prompt.sh +20 -0
- package/src/utils/templates/prompt.yaml +8 -0
- package/src/utils/templates/workflow.yaml +69 -0
package/bin/pullsmith.js
CHANGED
|
@@ -1,2 +1,23 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
2
|
+
|
|
3
|
+
import { init } from "../src/commands/init.js"
|
|
4
|
+
import { validate } from "../src/commands/validate.js"
|
|
5
|
+
import { doctor } from "../src/commands/doctor.js"
|
|
6
|
+
// import { push } from "../src/commands/push.js"
|
|
7
|
+
|
|
8
|
+
const command = process.argv[2];
|
|
9
|
+
|
|
10
|
+
if (command == "init"){
|
|
11
|
+
init();
|
|
12
|
+
} else if(command == "validate"){
|
|
13
|
+
validate();
|
|
14
|
+
} else if(command == "doctor"){
|
|
15
|
+
doctor();
|
|
16
|
+
} else {
|
|
17
|
+
console.log("Usage: pullsmith <command>");
|
|
18
|
+
console.log("Commands: init, validate, doctor");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
// console.log("pullsmith v0.0.1");
|
package/package.json
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pullsmith",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.0.2",
|
|
4
|
+
"description": "Run AI agents in your CI/CD pipeline with a single YAML file.",
|
|
5
|
+
"homepage": "https://pullsmith.dev",
|
|
6
|
+
"bugs": {
|
|
7
|
+
"url": "https://github.com/pullsmith/cli/issues"
|
|
8
|
+
},
|
|
5
9
|
"bin": {
|
|
6
10
|
"pullsmith": "./bin/pullsmith.js"
|
|
7
11
|
},
|
|
12
|
+
"type": "module",
|
|
8
13
|
"repository": {
|
|
9
14
|
"type": "git",
|
|
10
15
|
"url": "https://github.com/pullsmith/cli"
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { getToken } from "../lib/auth.js";
|
|
2
|
+
import { getRepoRemoteUrl } from "../lib/git.js";
|
|
3
|
+
import { PULLSMITH_BASE_URL } from "../lib/config.js";
|
|
4
|
+
|
|
5
|
+
export async function doctor() {
|
|
6
|
+
let token;
|
|
7
|
+
let repo;
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
token = getToken();
|
|
11
|
+
repo = getRepoRemoteUrl();
|
|
12
|
+
} catch (err) {
|
|
13
|
+
console.error(err.message);
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
const response = await fetch(`${PULLSMITH_BASE_URL}/api/doctor`, {
|
|
19
|
+
method: "POST",
|
|
20
|
+
headers: {
|
|
21
|
+
"Content-Type": "application/json",
|
|
22
|
+
"Authorization": `Bearer ${token}`
|
|
23
|
+
},
|
|
24
|
+
body: JSON.stringify({ repo })
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
if (!response.ok) {
|
|
28
|
+
const text = await response.text();
|
|
29
|
+
const error = getResponseError(text);
|
|
30
|
+
console.error(error ?? "Pullsmith doctor failed.");
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
console.log("Pullsmith doctor passed.");
|
|
35
|
+
} catch (err) {
|
|
36
|
+
console.error(err.message ?? "Pullsmith doctor failed.");
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function getResponseError(text) {
|
|
42
|
+
if (!text) return null;
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
const data = JSON.parse(text);
|
|
46
|
+
return data?.error ?? text;
|
|
47
|
+
} catch {
|
|
48
|
+
return text;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import http from "http";
|
|
2
|
+
import { exec } from "child_process";
|
|
3
|
+
import { getRepoRemoteUrl } from "../lib/git.js";
|
|
4
|
+
import { saveToken as saveTokenLocally } from "../lib/auth.js";
|
|
5
|
+
import { PULLSMITH_BASE_URL } from "../lib/config.js";
|
|
6
|
+
import { writeFileSync, readFileSync, existsSync, mkdirSync, rmSync } from "fs";
|
|
7
|
+
import { join } from "path";
|
|
8
|
+
|
|
9
|
+
const PORT = 9421;
|
|
10
|
+
|
|
11
|
+
export async function init() {
|
|
12
|
+
let repoUrl;
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
repoUrl = getRepoRemoteUrl();
|
|
16
|
+
} catch (err) {
|
|
17
|
+
console.error(err.message);
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const connectUrl = `${PULLSMITH_BASE_URL}/connect?repo=${encodeURIComponent(repoUrl)}`;
|
|
22
|
+
|
|
23
|
+
console.log("Opening browser to authenticate...");
|
|
24
|
+
|
|
25
|
+
if (process.platform === "darwin") {
|
|
26
|
+
// macOS
|
|
27
|
+
exec(`open "${connectUrl}"`);
|
|
28
|
+
} else if (process.platform === "win32") {
|
|
29
|
+
exec(`start "" "${connectUrl}"`);
|
|
30
|
+
} else {
|
|
31
|
+
exec(`xdg-open "${connectUrl}"`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
await waitForNextJsInternalToken(repoUrl);
|
|
37
|
+
} catch (err) {
|
|
38
|
+
console.error(err.message);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function createPullsmithFile(){
|
|
44
|
+
try {
|
|
45
|
+
const filePath = join(process.cwd(), ".pullsmith");
|
|
46
|
+
|
|
47
|
+
if (existsSync(filePath)) {
|
|
48
|
+
console.log(".pullsmith file already exits, skipping.");
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const promptPath = new URL("../../src/utils/templates/prompt.yaml", import.meta.url);
|
|
53
|
+
const prompt = readFileSync(promptPath, "utf8");
|
|
54
|
+
|
|
55
|
+
writeFileSync(filePath, prompt, "utf8");
|
|
56
|
+
|
|
57
|
+
console.log(".pullsmith file created!");
|
|
58
|
+
} catch (err) {
|
|
59
|
+
throw new Error("☹️ Failed to create local .pullsmith file.");
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
function getGithubWorkflowContent(secretName) {
|
|
65
|
+
const templatePath = new URL("../../src/utils/templates/workflow.yaml", import.meta.url);
|
|
66
|
+
return readFileSync(templatePath, "utf8").replaceAll("__SECRET_NAME__", secretName);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
function createGithubWorkflowFile(secretName) {
|
|
71
|
+
try {
|
|
72
|
+
const dirPath = join(process.cwd(), ".github", "workflows");
|
|
73
|
+
const filePath = join(dirPath, "pullsmith.yaml");
|
|
74
|
+
|
|
75
|
+
if (existsSync(filePath)) {
|
|
76
|
+
console.log("Workflow file already exists, skipping.");
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
mkdirSync(dirPath, { recursive: true });
|
|
81
|
+
|
|
82
|
+
writeFileSync(filePath, getGithubWorkflowContent(secretName), "utf8");
|
|
83
|
+
|
|
84
|
+
console.log("✅ Workflow file created at .github/workflows/pullsmith.yml");
|
|
85
|
+
} catch (err) {
|
|
86
|
+
throw new Error(`Failed to create Github workflow file: ${err.message}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function deleteWorkflowFile() {
|
|
91
|
+
try {
|
|
92
|
+
// .github/workflows/pullsmith.yml
|
|
93
|
+
const filePath = join(process.cwd(), ".github", "workflows", "pullsmith.yml");
|
|
94
|
+
if (existsSync(filePath)) {
|
|
95
|
+
rmSync(filePath);
|
|
96
|
+
console.log("🗑️ Removed old workflow file.");
|
|
97
|
+
}
|
|
98
|
+
} catch (err) {
|
|
99
|
+
throw new Error("☹️ Failed to delete local .pullsmith file.");
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// We only get this token if Github auth is successful
|
|
104
|
+
function waitForNextJsInternalToken(repoUrl) {
|
|
105
|
+
return new Promise((resolve, reject) => {
|
|
106
|
+
const server = http.createServer(async (req, res) => {
|
|
107
|
+
const url = new URL(req.url, `http://localhost:${PORT}`);
|
|
108
|
+
const internalNextJSToken = url.searchParams.get("token");
|
|
109
|
+
|
|
110
|
+
if (internalNextJSToken) {
|
|
111
|
+
saveTokenLocally(internalNextJSToken);
|
|
112
|
+
deleteWorkflowFile();
|
|
113
|
+
createPullsmithFile();
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
const secretName = await checkThatClaudeAPIKeyIsInSecrets(internalNextJSToken, repoUrl);
|
|
117
|
+
createGithubWorkflowFile(secretName);
|
|
118
|
+
|
|
119
|
+
res.writeHead(200);
|
|
120
|
+
res.end("Authentication successful! You can close this tab.");
|
|
121
|
+
server.close();
|
|
122
|
+
console.log("Authenticated successfully!");
|
|
123
|
+
resolve();
|
|
124
|
+
} catch (err) {
|
|
125
|
+
reject(err);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
server.listen(PORT, () => {
|
|
131
|
+
console.log(`Waiting for authentication on port ${PORT}...`);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
server.on("error", reject);
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
const missingClaudeSecretsMsg = (secretUrl) => `❌ No Anthropic API key found in Github Actions secrets.\n
|
|
140
|
+
Please add your Claude API key at:
|
|
141
|
+
${secretUrl}
|
|
142
|
+
Make sure to name it: ANTHROPIC_API_KEY or CLAUDE_API_KEY
|
|
143
|
+
`;
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
async function checkThatClaudeAPIKeyIsInSecrets(nextJsInternalToken, repoUrl) {
|
|
147
|
+
try {
|
|
148
|
+
const response = await fetch(`${PULLSMITH_BASE_URL}/api/init`, {
|
|
149
|
+
method: "POST",
|
|
150
|
+
headers: {
|
|
151
|
+
"Content-Type": "application/json",
|
|
152
|
+
"Authorization": `Bearer ${nextJsInternalToken}`
|
|
153
|
+
},
|
|
154
|
+
body: JSON.stringify({ repo: repoUrl })
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const data = await response.json();
|
|
158
|
+
|
|
159
|
+
if (!response.ok) {
|
|
160
|
+
// if the github token access has expired this fires
|
|
161
|
+
throw new Error(data.error ?? "Problem with Github Auth") ;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (data.secretName) {
|
|
165
|
+
console.log(`✅ You have a Claude API key in Github Actions: ${data.secretName}`);
|
|
166
|
+
return data.secretName;
|
|
167
|
+
} else {
|
|
168
|
+
throw new Error(missingClaudeSecretsMsg(data.secretsUrl))
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
} catch (err) {
|
|
172
|
+
throw new Error(err.message ?? 'Failed to find Claude API key in Github Actions.');
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// async function checkAnthropicSecret(repoUrl, token) {
|
|
177
|
+
// const { owner, repo } = parseRepoInfo(repoUrl);
|
|
178
|
+
//
|
|
179
|
+
// const response = await fetch(`https://api.github.com/repos/${owner}/${repo}/actions/secrets`, {
|
|
180
|
+
// headers: {
|
|
181
|
+
// "Authorization": `Bearer ${token}`,
|
|
182
|
+
// "Accept": "application/vnd.github+json"
|
|
183
|
+
// }
|
|
184
|
+
// });
|
|
185
|
+
//
|
|
186
|
+
// const data = await response.json();
|
|
187
|
+
// const secrets = data.secrets || [];
|
|
188
|
+
//
|
|
189
|
+
// const hasKey = secrets.some(s =>
|
|
190
|
+
// s.name.includes("ANTHROPIC") || s.name.includes("CLAUDE")
|
|
191
|
+
// );
|
|
192
|
+
//
|
|
193
|
+
// if (!hasKey) {
|
|
194
|
+
// const secretsUrl = `https://github.com/${owner}/${repo}/settings/secrets/actions/new`;
|
|
195
|
+
// console.log(`\n⚠️ No Anthropic API key found in your repo secrets.`);
|
|
196
|
+
// console.log(` Please add your API key at:`);
|
|
197
|
+
// console.log(` ${secretsUrl}`);
|
|
198
|
+
// console.log(` Name it: ANTHROPIC_API_KEY or CLAUDE_API_KEY\n`);
|
|
199
|
+
// } else {
|
|
200
|
+
// console.log("✅ Anthropic API key found in repo secrets.");
|
|
201
|
+
// }
|
|
202
|
+
// }
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { PULLSMITH_BASE_URL } from "../lib/config.js";
|
|
4
|
+
|
|
5
|
+
export async function validate() {
|
|
6
|
+
const filePath = join(process.cwd(), ".pullsmith");
|
|
7
|
+
|
|
8
|
+
if (!existsSync(filePath)) {
|
|
9
|
+
console.error("Missing .pullsmith file. Run `pullsmith init` first.");
|
|
10
|
+
process.exit(1);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const file = readFileSync(filePath, "utf8");
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
const response = await fetch(`${PULLSMITH_BASE_URL}/api/validate`, {
|
|
17
|
+
method: "POST",
|
|
18
|
+
headers: {
|
|
19
|
+
"Content-Type": "application/json"
|
|
20
|
+
},
|
|
21
|
+
body: JSON.stringify({ file })
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
if (!response.ok) {
|
|
25
|
+
const text = await response.text();
|
|
26
|
+
const error = getResponseError(text);
|
|
27
|
+
console.error(error ?? "Failed to validate .pullsmith file.");
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
console.log(".pullsmith is valid.");
|
|
32
|
+
} catch (err) {
|
|
33
|
+
console.error(err.message ?? "Failed to validate .pullsmith file.");
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function getResponseError(text) {
|
|
39
|
+
if (!text) return null;
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const data = JSON.parse(text);
|
|
43
|
+
return data?.error ?? text;
|
|
44
|
+
} catch {
|
|
45
|
+
return text;
|
|
46
|
+
}
|
|
47
|
+
}
|
package/src/lib/auth.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync, readFileSync } from "fs";
|
|
2
|
+
import { homedir } from "os";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
|
|
5
|
+
const credentialsPath = join(homedir(), ".pullsmith", "credentials");
|
|
6
|
+
|
|
7
|
+
// Saves generated token from Pullsmith.dev in local machine ./pullsmith/credentials
|
|
8
|
+
export function saveToken(token) {
|
|
9
|
+
try {
|
|
10
|
+
mkdirSync(join(homedir(), ".pullsmith"), { recursive: true });
|
|
11
|
+
writeFileSync(credentialsPath, JSON.stringify({ token }), "utf8");
|
|
12
|
+
} catch (err) {
|
|
13
|
+
throw new Error("☹️ Failed to save local credentials in .pullsmith file.");
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Reads the token so the CLI can make authenticated requests to Pullsmith's API
|
|
18
|
+
export function getToken() {
|
|
19
|
+
try {
|
|
20
|
+
const file = readFileSync(credentialsPath, "utf8");
|
|
21
|
+
return JSON.parse(file).token;
|
|
22
|
+
} catch {
|
|
23
|
+
throw new Error("Not authenticated. Run `pullsmith init` first.");
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const PULLSMITH_BASE_URL = process.env.PULLSMITH_BASE_URL ?? "https://pullsmith.dev";
|
package/src/lib/git.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { execSync } from "child_process";
|
|
2
|
+
|
|
3
|
+
export function getRepoRemoteUrl(){
|
|
4
|
+
try {
|
|
5
|
+
return execSync("git remote get-url origin", { stdio: "pipe" }).toString().trim();
|
|
6
|
+
} catch (err) {
|
|
7
|
+
throw new Error("☹️ Looks like you're not inside a Git repo. cd into your project, then try again.");
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function parseRepoInfo(remoteUrl) {
|
|
12
|
+
const match = remoteUrl.match(/github\.com[:/](.+?)\/(.+?)(\.git)?$/);
|
|
13
|
+
if (!match) throw new Error("Could not parse GitHub repo info from remote URL.");
|
|
14
|
+
return { owner: match[1], repo: match[2] };
|
|
15
|
+
}
|
|
16
|
+
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
|
|
3
|
+
AGENT=$(awk -F': *' '/^sentry_agent:/{print $2; exit}' .pullsmith)
|
|
4
|
+
|
|
5
|
+
MODEL=$(awk -v a="$AGENT" '
|
|
6
|
+
/^[[:space:]]*-?[[:space:]]*name:[[:space:]]*/ {
|
|
7
|
+
name=$0; sub(/^[[:space:]]*-?[[:space:]]*name:[[:space:]]*/,"",name)
|
|
8
|
+
here=(name==a); next
|
|
9
|
+
}
|
|
10
|
+
here && /^[[:space:]]*model:[[:space:]]*/ {
|
|
11
|
+
m=$0; sub(/^[[:space:]]*model:[[:space:]]*/,"",m)
|
|
12
|
+
sub(/[[:space:]]*(#.*)?$/,"",m) # strip trailing spaces/comments
|
|
13
|
+
print m; exit
|
|
14
|
+
}
|
|
15
|
+
' .pullsmith)
|
|
16
|
+
|
|
17
|
+
printf '%s\n' "$MODEL"
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
|
|
3
|
+
AGENT=$(awk -F': *' '/^sentry_agent:/{print $2; exit}' .pullsmith)
|
|
4
|
+
|
|
5
|
+
PROMPT=$(awk -v a="$AGENT" '
|
|
6
|
+
/^[[:space:]]*-?[[:space:]]*name:[[:space:]]*/ {
|
|
7
|
+
name=$0; sub(/^[[:space:]]*-?[[:space:]]*name:[[:space:]]*/,"",name)
|
|
8
|
+
here=(name==a); inp=0; next
|
|
9
|
+
}
|
|
10
|
+
here && /prompt:[[:space:]]*\|/ { inp=1; ind=-1; next }
|
|
11
|
+
inp {
|
|
12
|
+
if ($0 ~ /^[[:space:]]*$/) { print ""; next }
|
|
13
|
+
n=match($0,/[^[:space:]]/)-1
|
|
14
|
+
if (ind<0) ind=n
|
|
15
|
+
if (n<ind) { inp=0; next }
|
|
16
|
+
print substr($0, ind+1)
|
|
17
|
+
}
|
|
18
|
+
' .pullsmith)
|
|
19
|
+
|
|
20
|
+
printf '%s\n' "$PROMPT"
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
name: Pullsmith
|
|
2
|
+
on:
|
|
3
|
+
workflow_dispatch:
|
|
4
|
+
inputs:
|
|
5
|
+
error:
|
|
6
|
+
description: "Sentry error title"
|
|
7
|
+
required: false
|
|
8
|
+
default: "Sentry error"
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
run:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
permissions:
|
|
14
|
+
contents: write
|
|
15
|
+
pull-requests: write
|
|
16
|
+
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v4
|
|
19
|
+
- name: Run Claude Code
|
|
20
|
+
env:
|
|
21
|
+
ANTHROPIC_API_KEY: ${{ secrets.__SECRET_NAME__ }}
|
|
22
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
23
|
+
run: |
|
|
24
|
+
ID=$(gh api "/users/pullsmith-dev[bot]" --jq .id)
|
|
25
|
+
git config user.email "${ID}+pullsmith-dev[bot]@users.noreply.github.com"
|
|
26
|
+
git config user.name "pullsmith-dev[bot]"
|
|
27
|
+
ERROR="${{ github.event.inputs.error }}"
|
|
28
|
+
SLUG=$(echo "$ERROR" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g; s/--*/-/g; s/^-//; s/-$//' | cut -c1-50)
|
|
29
|
+
BRANCH="pullsmith/fix-$SLUG"
|
|
30
|
+
AGENT=$(awk -F': *' '/^sentry_agent:/{print $2; exit}' .pullsmith)
|
|
31
|
+
PROMPT=$(awk -v a="$AGENT" '
|
|
32
|
+
/^[[:space:]]*-?[[:space:]]*name:[[:space:]]*/ {
|
|
33
|
+
name=$0; sub(/^[[:space:]]*-?[[:space:]]*name:[[:space:]]*/,"",name)
|
|
34
|
+
here=(name==a); inp=0; next
|
|
35
|
+
}
|
|
36
|
+
here && /prompt:[[:space:]]*|/ { inp=1; ind=-1; next }
|
|
37
|
+
inp {
|
|
38
|
+
if ($0 ~ /^[[:space:]]*$/) { print ""; next }
|
|
39
|
+
n=match($0,/[^[:space:]]/)-1
|
|
40
|
+
if (ind<0) ind=n
|
|
41
|
+
if (n<ind) { inp=0; next }
|
|
42
|
+
print substr($0, ind+1)
|
|
43
|
+
}
|
|
44
|
+
' .pullsmith)
|
|
45
|
+
MODEL=$(awk -v a="$AGENT" '
|
|
46
|
+
/^[[:space:]]*-?[[:space:]]*name:[[:space:]]*/ {
|
|
47
|
+
name=$0; sub(/^[[:space:]]*-?[[:space:]]*name:[[:space:]]*/,"",name)
|
|
48
|
+
here=(name==a); next
|
|
49
|
+
}
|
|
50
|
+
here && /^[[:space:]]*model:[[:space:]]*/ {
|
|
51
|
+
m=$0; sub(/^[[:space:]]*model:[[:space:]]*/,"",m)
|
|
52
|
+
sub(/[[:space:]]*(#.*)?$/,"",m) # strip trailing spaces/comments
|
|
53
|
+
print m; exit
|
|
54
|
+
}
|
|
55
|
+
' .pullsmith)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
git checkout -b "$BRANCH"
|
|
59
|
+
npx @anthropic-ai/claude-code -p --model "$MODEL" --dangerously-skip-permissions "$PROMPT. You are fixing this Sentry error: $ERROR. ONLY edit the code in the working tree to fix it. Do NOT run git, do NOT commit, do NOT push, and do NOT open a pull request - the surrounding workflow handles all of that."
|
|
60
|
+
|
|
61
|
+
git add -A
|
|
62
|
+
if git diff --cached --quiet; then
|
|
63
|
+
echo "No changes produced; nothing to do."
|
|
64
|
+
exit 0;
|
|
65
|
+
fi
|
|
66
|
+
|
|
67
|
+
git commit -m "fix: $ERROR"
|
|
68
|
+
git push origin "$BRANCH"
|
|
69
|
+
gh pr create --title "$ERROR" --body "Automated fix by Pullsmith for Sentry error: $ERROR" --base "${{ github.ref_name }}" --head "$BRANCH"
|