@iloom/cli 0.4.1 → 0.5.1
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/README.md +24 -0
- package/dist/{ClaudeContextManager-DK77227F.js → ClaudeContextManager-DQFKIMEP.js} +5 -5
- package/dist/{ClaudeService-W3SA7HVG.js → ClaudeService-CJS32WG2.js} +4 -4
- package/dist/{LoomLauncher-S3YGJRJQ.js → LoomLauncher-JNWBMHES.js} +22 -88
- package/dist/LoomLauncher-JNWBMHES.js.map +1 -0
- package/dist/MetadataManager-WXUVXKUS.js +10 -0
- package/dist/PRManager-7DSIMCAD.js +16 -0
- package/dist/{PromptTemplateManager-2TDZAUC6.js → PromptTemplateManager-72FEOGT6.js} +2 -2
- package/dist/README.md +24 -0
- package/dist/{SettingsManager-FJFU6JJD.js → SettingsManager-XPR4TEQL.js} +2 -2
- package/dist/agents/iloom-issue-analyze-and-plan.md +41 -7
- package/dist/agents/iloom-issue-analyzer.md +38 -8
- package/dist/agents/iloom-issue-complexity-evaluator.md +45 -15
- package/dist/agents/iloom-issue-enhancer.md +60 -18
- package/dist/agents/iloom-issue-implementer.md +29 -7
- package/dist/agents/iloom-issue-planner.md +36 -7
- package/dist/agents/iloom-issue-reviewer.md +30 -7
- package/dist/{chunk-JC5HXN75.js → chunk-3CMGCRB5.js} +2 -2
- package/dist/{chunk-G6CIIJLT.js → chunk-4YTILIIH.js} +7 -8
- package/dist/chunk-4YTILIIH.js.map +1 -0
- package/dist/{chunk-IARWMDAX.js → chunk-6KB7R22U.js} +98 -16
- package/dist/chunk-6KB7R22U.js.map +1 -0
- package/dist/{chunk-55TB3FSG.js → chunk-AS2IRKLU.js} +2 -2
- package/dist/{chunk-VTXCGKV5.js → chunk-BVIK2P6P.js} +11 -3
- package/dist/chunk-BVIK2P6P.js.map +1 -0
- package/dist/chunk-CDF7ZX2B.js +72 -0
- package/dist/chunk-CDF7ZX2B.js.map +1 -0
- package/dist/{chunk-POI7KLBH.js → chunk-CDQEK2WD.js} +5 -5
- package/dist/{chunk-74VMN2KC.js → chunk-DKQ4SUII.js} +16 -1
- package/dist/chunk-DKQ4SUII.js.map +1 -0
- package/dist/{chunk-BIIQHEXJ.js → chunk-GVRO4PWE.js} +12 -8
- package/dist/chunk-GVRO4PWE.js.map +1 -0
- package/dist/{chunk-TMZAVPGF.js → chunk-HABINPX2.js} +71 -15
- package/dist/{chunk-TMZAVPGF.js.map → chunk-HABINPX2.js.map} +1 -1
- package/dist/{chunk-2W2FBL5G.js → chunk-LN4H3A6A.js} +66 -7
- package/dist/chunk-LN4H3A6A.js.map +1 -0
- package/dist/{chunk-VWNS6DH5.js → chunk-OOU3DKNT.js} +13 -7
- package/dist/chunk-OOU3DKNT.js.map +1 -0
- package/dist/chunk-P2ZQ5LKB.js +347 -0
- package/dist/chunk-P2ZQ5LKB.js.map +1 -0
- package/dist/{chunk-OF7BNW4D.js → chunk-RJKMF6BC.js} +30 -4
- package/dist/chunk-RJKMF6BC.js.map +1 -0
- package/dist/{chunk-O7WHXLCB.js → chunk-RNZMHJK7.js} +18 -4
- package/dist/chunk-RNZMHJK7.js.map +1 -0
- package/dist/{chunk-UPUAQYAW.js → chunk-S65T4O6I.js} +2 -2
- package/dist/{chunk-HD5SUKI2.js → chunk-TSLKDFAF.js} +55 -6
- package/dist/chunk-TSLKDFAF.js.map +1 -0
- package/dist/{chunk-IJ7IGJT3.js → chunk-YZTDGPFB.js} +18 -1
- package/dist/chunk-YZTDGPFB.js.map +1 -0
- package/dist/{cleanup-KDLVTT7M.js → cleanup-LU6NU2NZ.js} +14 -14
- package/dist/cli.js +283 -363
- package/dist/cli.js.map +1 -1
- package/dist/{contribute-HY372S6F.js → contribute-RS3DO3WP.js} +4 -4
- package/dist/{dev-server-JCJGQ3PV.js → dev-server-ASH7HJVI.js} +30 -16
- package/dist/dev-server-ASH7HJVI.js.map +1 -0
- package/dist/{feedback-7PVBQNLJ.js → feedback-OFVW22UW.js} +11 -6
- package/dist/{feedback-7PVBQNLJ.js.map → feedback-OFVW22UW.js.map} +1 -1
- package/dist/{git-4BVOOOOV.js → git-OQAPUPLP.js} +16 -6
- package/dist/git-OQAPUPLP.js.map +1 -0
- package/dist/{ignite-3B264M7K.js → ignite-NREQ3JRM.js} +57 -22
- package/dist/ignite-NREQ3JRM.js.map +1 -0
- package/dist/index.d.ts +58 -7
- package/dist/index.js +110 -7
- package/dist/index.js.map +1 -1
- package/dist/{init-LBA6NUK2.js → init-F6PFMSU5.js} +7 -7
- package/dist/init-F6PFMSU5.js.map +1 -0
- package/dist/mcp/recap-server.js +264 -0
- package/dist/mcp/recap-server.js.map +1 -0
- package/dist/{open-OGCV32Z4.js → open-KW4NTLXH.js} +16 -17
- package/dist/{open-OGCV32Z4.js.map → open-KW4NTLXH.js.map} +1 -1
- package/dist/{projects-P55273AB.js → projects-QEAEBAT2.js} +2 -2
- package/dist/prompts/init-prompt.txt +31 -72
- package/dist/prompts/issue-prompt.txt +115 -15
- package/dist/prompts/pr-prompt.txt +49 -1
- package/dist/prompts/regular-prompt.txt +80 -20
- package/dist/{rebase-4T5FQHNH.js → rebase-WZHHE5LU.js} +6 -6
- package/dist/recap-33NPZ3ZO.js +117 -0
- package/dist/recap-33NPZ3ZO.js.map +1 -0
- package/dist/{run-HNOP6WE2.js → run-HRYQ7TR7.js} +16 -17
- package/dist/{run-HNOP6WE2.js.map → run-HRYQ7TR7.js.map} +1 -1
- package/dist/schema/settings.schema.json +13 -2
- package/dist/{shell-DE3HKJSM.js → shell-JMU5XTHW.js} +6 -6
- package/dist/{summary-GDT7DTRI.js → summary-4SSGGH7N.js} +17 -9
- package/dist/summary-4SSGGH7N.js.map +1 -0
- package/dist/{test-git-YMAE57UP.js → test-git-6SAIRBUD.js} +4 -4
- package/dist/{test-prefix-YCKL6CMT.js → test-prefix-RLVRK5ZD.js} +4 -4
- package/package.json +1 -1
- package/dist/LoomLauncher-S3YGJRJQ.js.map +0 -1
- package/dist/chunk-2W2FBL5G.js.map +0 -1
- package/dist/chunk-74VMN2KC.js.map +0 -1
- package/dist/chunk-BIIQHEXJ.js.map +0 -1
- package/dist/chunk-G6CIIJLT.js.map +0 -1
- package/dist/chunk-HD5SUKI2.js.map +0 -1
- package/dist/chunk-IARWMDAX.js.map +0 -1
- package/dist/chunk-IJ7IGJT3.js.map +0 -1
- package/dist/chunk-O7WHXLCB.js.map +0 -1
- package/dist/chunk-OF7BNW4D.js.map +0 -1
- package/dist/chunk-QRBOPFAA.js +0 -48
- package/dist/chunk-QRBOPFAA.js.map +0 -1
- package/dist/chunk-VTXCGKV5.js.map +0 -1
- package/dist/chunk-VWNS6DH5.js.map +0 -1
- package/dist/dev-server-JCJGQ3PV.js.map +0 -1
- package/dist/ignite-3B264M7K.js.map +0 -1
- package/dist/summary-GDT7DTRI.js.map +0 -1
- /package/dist/{ClaudeContextManager-DK77227F.js.map → ClaudeContextManager-DQFKIMEP.js.map} +0 -0
- /package/dist/{ClaudeService-W3SA7HVG.js.map → ClaudeService-CJS32WG2.js.map} +0 -0
- /package/dist/{PromptTemplateManager-2TDZAUC6.js.map → MetadataManager-WXUVXKUS.js.map} +0 -0
- /package/dist/{SettingsManager-FJFU6JJD.js.map → PRManager-7DSIMCAD.js.map} +0 -0
- /package/dist/{git-4BVOOOOV.js.map → PromptTemplateManager-72FEOGT6.js.map} +0 -0
- /package/dist/{init-LBA6NUK2.js.map → SettingsManager-XPR4TEQL.js.map} +0 -0
- /package/dist/{chunk-JC5HXN75.js.map → chunk-3CMGCRB5.js.map} +0 -0
- /package/dist/{chunk-55TB3FSG.js.map → chunk-AS2IRKLU.js.map} +0 -0
- /package/dist/{chunk-POI7KLBH.js.map → chunk-CDQEK2WD.js.map} +0 -0
- /package/dist/{chunk-UPUAQYAW.js.map → chunk-S65T4O6I.js.map} +0 -0
- /package/dist/{cleanup-KDLVTT7M.js.map → cleanup-LU6NU2NZ.js.map} +0 -0
- /package/dist/{contribute-HY372S6F.js.map → contribute-RS3DO3WP.js.map} +0 -0
- /package/dist/{projects-P55273AB.js.map → projects-QEAEBAT2.js.map} +0 -0
- /package/dist/{rebase-4T5FQHNH.js.map → rebase-WZHHE5LU.js.map} +0 -0
- /package/dist/{shell-DE3HKJSM.js.map → shell-JMU5XTHW.js.map} +0 -0
- /package/dist/{test-git-YMAE57UP.js.map → test-git-6SAIRBUD.js.map} +0 -0
- /package/dist/{test-prefix-YCKL6CMT.js.map → test-prefix-RLVRK5ZD.js.map} +0 -0
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
openBrowser
|
|
4
|
+
} from "./chunk-YETJNRQM.js";
|
|
5
|
+
import {
|
|
6
|
+
getConfiguredRepoFromSettings,
|
|
7
|
+
getEffectivePRTargetRemote,
|
|
8
|
+
parseGitRemotes
|
|
9
|
+
} from "./chunk-PSFVTBM7.js";
|
|
10
|
+
import {
|
|
11
|
+
executeGhCommand
|
|
12
|
+
} from "./chunk-KO2FOMHL.js";
|
|
13
|
+
import {
|
|
14
|
+
detectClaudeCli,
|
|
15
|
+
launchClaude
|
|
16
|
+
} from "./chunk-RUC7OULH.js";
|
|
17
|
+
import {
|
|
18
|
+
getLogger
|
|
19
|
+
} from "./chunk-6UIGZD2N.js";
|
|
20
|
+
|
|
21
|
+
// src/lib/PRManager.ts
|
|
22
|
+
var PRManager = class {
|
|
23
|
+
constructor(settings) {
|
|
24
|
+
this.settings = settings;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Check if a PR already exists for the given branch
|
|
28
|
+
* @param branchName - Branch to check
|
|
29
|
+
* @param cwd - Working directory
|
|
30
|
+
* @returns Existing PR info or null if none found
|
|
31
|
+
*/
|
|
32
|
+
async checkForExistingPR(branchName, cwd) {
|
|
33
|
+
try {
|
|
34
|
+
const prList = await executeGhCommand(
|
|
35
|
+
["pr", "list", "--head", branchName, "--state", "open", "--json", "number,url"],
|
|
36
|
+
cwd ? { cwd } : void 0
|
|
37
|
+
);
|
|
38
|
+
if (prList.length > 0) {
|
|
39
|
+
return prList[0] ?? null;
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
} catch (error) {
|
|
43
|
+
getLogger().debug("Error checking for existing PR", { error });
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Generate PR body using Claude if available, otherwise use simple template
|
|
49
|
+
* @param issueNumber - Issue number to include in body
|
|
50
|
+
* @param worktreePath - Path to worktree for context
|
|
51
|
+
* @returns PR body markdown
|
|
52
|
+
*/
|
|
53
|
+
async generatePRBody(issueNumber, worktreePath) {
|
|
54
|
+
const hasClaudeCli = await detectClaudeCli();
|
|
55
|
+
if (hasClaudeCli) {
|
|
56
|
+
try {
|
|
57
|
+
const prompt = this.buildPRBodyPrompt(issueNumber);
|
|
58
|
+
const body2 = await launchClaude(prompt, {
|
|
59
|
+
headless: true,
|
|
60
|
+
addDir: worktreePath,
|
|
61
|
+
timeout: 3e4
|
|
62
|
+
});
|
|
63
|
+
if (body2 && typeof body2 === "string" && body2.trim()) {
|
|
64
|
+
const sanitized = this.sanitizeClaudeOutput(body2);
|
|
65
|
+
if (sanitized) {
|
|
66
|
+
return sanitized;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
} catch (error) {
|
|
70
|
+
getLogger().debug("Claude PR body generation failed, using template", { error });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
let body = "This PR contains changes from the iloom workflow.\n\n";
|
|
74
|
+
if (issueNumber) {
|
|
75
|
+
body += `Fixes #${issueNumber}`;
|
|
76
|
+
}
|
|
77
|
+
return body;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Build structured XML prompt for PR body generation
|
|
81
|
+
* Uses XML format for clear task definition and output expectations
|
|
82
|
+
*/
|
|
83
|
+
buildPRBodyPrompt(issueNumber) {
|
|
84
|
+
const issueContext = issueNumber ? `
|
|
85
|
+
<IssueContext>
|
|
86
|
+
This PR is associated with GitHub issue #${issueNumber}.
|
|
87
|
+
Include "Fixes #${issueNumber}" at the end of the body on its own line.
|
|
88
|
+
</IssueContext>` : "";
|
|
89
|
+
return `<Task>
|
|
90
|
+
You are a software engineer writing a pull request body for this repository.
|
|
91
|
+
Examine the changes in the git repository and generate a concise, professional PR description.
|
|
92
|
+
</Task>
|
|
93
|
+
|
|
94
|
+
<Requirements>
|
|
95
|
+
<Format>Write 2-3 sentences summarizing what was changed and why.${issueNumber ? `
|
|
96
|
+
|
|
97
|
+
End with "Fixes #${issueNumber}" on its own line.` : ""}</Format>
|
|
98
|
+
<Tone>Professional and concise</Tone>
|
|
99
|
+
<Focus>Summarize the changes and their purpose</Focus>
|
|
100
|
+
<NoMeta>CRITICAL: Do NOT include ANY explanatory text, analysis, or meta-commentary. Output ONLY the raw PR body text.</NoMeta>
|
|
101
|
+
<Examples>
|
|
102
|
+
Good: "Add user authentication with JWT tokens to secure the API endpoints. This includes login and registration endpoints with proper password hashing.
|
|
103
|
+
|
|
104
|
+
Fixes #42"
|
|
105
|
+
Good: "Fix navigation bug in sidebar menu that caused incorrect highlighting on nested routes."
|
|
106
|
+
Bad: "Here's the PR body:
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
Add user authentication..."
|
|
111
|
+
Bad: "Based on the changes, I'll write: Fix navigation bug..."
|
|
112
|
+
</Examples>
|
|
113
|
+
${issueContext}
|
|
114
|
+
</Requirements>
|
|
115
|
+
|
|
116
|
+
<Output>
|
|
117
|
+
IMPORTANT: Your entire response will be used directly as the GitHub pull request body.
|
|
118
|
+
Do not include any explanatory text, headers, or separators before or after the body.
|
|
119
|
+
Start your response immediately with the PR body text.
|
|
120
|
+
</Output>`;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Sanitize Claude output to remove meta-commentary and clean formatting
|
|
124
|
+
* Handles cases where Claude includes explanatory text despite instructions
|
|
125
|
+
*/
|
|
126
|
+
sanitizeClaudeOutput(rawOutput) {
|
|
127
|
+
let cleaned = rawOutput.trim();
|
|
128
|
+
const metaPatterns = [
|
|
129
|
+
/^.*?based on.*?changes.*?:/i,
|
|
130
|
+
/^.*?looking at.*?files.*?:/i,
|
|
131
|
+
/^.*?examining.*?:/i,
|
|
132
|
+
/^.*?analyzing.*?:/i,
|
|
133
|
+
/^.*?i'll.*?generate.*?:/i,
|
|
134
|
+
/^.*?let me.*?:/i,
|
|
135
|
+
/^.*?here.*?is.*?(?:the\s+)?(?:pr|pull request).*?body.*?:/i,
|
|
136
|
+
/^.*?here's.*?(?:the\s+)?(?:pr|pull request).*?body.*?:/i
|
|
137
|
+
];
|
|
138
|
+
for (const pattern of metaPatterns) {
|
|
139
|
+
cleaned = cleaned.replace(pattern, "").trim();
|
|
140
|
+
}
|
|
141
|
+
cleaned = cleaned.replace(/^[-=]{3,}\s*/m, "").trim();
|
|
142
|
+
if (cleaned.includes(":")) {
|
|
143
|
+
const colonIndex = cleaned.indexOf(":");
|
|
144
|
+
const beforeColon = cleaned.substring(0, colonIndex).trim().toLowerCase();
|
|
145
|
+
const metaIndicators = [
|
|
146
|
+
"here is the pr body",
|
|
147
|
+
"here is the pull request body",
|
|
148
|
+
"pr body",
|
|
149
|
+
"pull request body",
|
|
150
|
+
"here is",
|
|
151
|
+
"here's",
|
|
152
|
+
"the body should be",
|
|
153
|
+
"i suggest",
|
|
154
|
+
"my suggestion"
|
|
155
|
+
];
|
|
156
|
+
const isMetaCommentary = metaIndicators.some((indicator) => beforeColon.includes(indicator));
|
|
157
|
+
if (isMetaCommentary) {
|
|
158
|
+
const afterColon = cleaned.substring(colonIndex + 1).trim();
|
|
159
|
+
const afterSeparator = afterColon.replace(/^[-=]{3,}\s*/m, "").trim();
|
|
160
|
+
if (afterSeparator && afterSeparator.length > 10) {
|
|
161
|
+
cleaned = afterSeparator;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (cleaned.startsWith('"') && cleaned.endsWith('"') || cleaned.startsWith("'") && cleaned.endsWith("'")) {
|
|
166
|
+
cleaned = cleaned.slice(1, -1).trim();
|
|
167
|
+
}
|
|
168
|
+
return cleaned;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Create a GitHub PR for the branch
|
|
172
|
+
* @param branchName - Branch to create PR from (used as --head)
|
|
173
|
+
* @param title - PR title
|
|
174
|
+
* @param body - PR body
|
|
175
|
+
* @param baseBranch - Base branch to target (usually main/master)
|
|
176
|
+
* @param cwd - Working directory
|
|
177
|
+
* @returns PR URL
|
|
178
|
+
*/
|
|
179
|
+
async createPR(branchName, title, body, baseBranch, cwd) {
|
|
180
|
+
try {
|
|
181
|
+
const targetRemote = await getEffectivePRTargetRemote(this.settings, cwd);
|
|
182
|
+
let headValue = branchName;
|
|
183
|
+
if (targetRemote !== "origin") {
|
|
184
|
+
const remotes = await parseGitRemotes(cwd);
|
|
185
|
+
const originRemote = remotes.find((r) => r.name === "origin");
|
|
186
|
+
if (originRemote) {
|
|
187
|
+
headValue = `${originRemote.owner}:${branchName}`;
|
|
188
|
+
getLogger().debug(`Fork workflow detected, using head: ${headValue}`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
const args = ["pr", "create", "--head", headValue, "--title", title, "--body", body, "--base", baseBranch];
|
|
192
|
+
if (targetRemote !== "origin") {
|
|
193
|
+
const repo = await getConfiguredRepoFromSettings(this.settings, cwd);
|
|
194
|
+
args.push("--repo", repo);
|
|
195
|
+
}
|
|
196
|
+
const result = await executeGhCommand(args, cwd ? { cwd } : void 0);
|
|
197
|
+
const url = typeof result === "string" ? result.trim() : String(result).trim();
|
|
198
|
+
if (!url.includes("github.com") || !url.includes("/pull/")) {
|
|
199
|
+
throw new Error(`Unexpected response from gh pr create: ${url}`);
|
|
200
|
+
}
|
|
201
|
+
return url;
|
|
202
|
+
} catch (error) {
|
|
203
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
204
|
+
if (errorMessage.includes("Head sha can't be blank") || errorMessage.includes("No commits between")) {
|
|
205
|
+
throw new Error(
|
|
206
|
+
`Failed to create pull request: ${errorMessage}
|
|
207
|
+
|
|
208
|
+
This error typically occurs when:
|
|
209
|
+
- The branch was not fully pushed to the remote
|
|
210
|
+
- There's a race condition between push and PR creation
|
|
211
|
+
- The branch has no commits ahead of the base branch
|
|
212
|
+
|
|
213
|
+
Try running: git push -u origin ${branchName}
|
|
214
|
+
Then retry: il finish`
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
throw new Error(`Failed to create pull request: ${errorMessage}`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Open PR URL in browser
|
|
222
|
+
* @param url - PR URL to open
|
|
223
|
+
*/
|
|
224
|
+
async openPRInBrowser(url) {
|
|
225
|
+
try {
|
|
226
|
+
await openBrowser(url);
|
|
227
|
+
getLogger().debug("Opened PR in browser", { url });
|
|
228
|
+
} catch (error) {
|
|
229
|
+
getLogger().warn("Failed to open PR in browser", { error });
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Complete PR workflow: check for existing, create if needed, optionally open in browser
|
|
234
|
+
* @param branchName - Branch to create PR from
|
|
235
|
+
* @param title - PR title
|
|
236
|
+
* @param issueNumber - Optional issue number for body generation
|
|
237
|
+
* @param baseBranch - Base branch to target
|
|
238
|
+
* @param worktreePath - Path to worktree
|
|
239
|
+
* @param openInBrowser - Whether to open PR in browser
|
|
240
|
+
* @returns PR creation result
|
|
241
|
+
*/
|
|
242
|
+
async createOrOpenPR(branchName, title, issueNumber, baseBranch, worktreePath, openInBrowser) {
|
|
243
|
+
const existingPR = await this.checkForExistingPR(branchName, worktreePath);
|
|
244
|
+
if (existingPR) {
|
|
245
|
+
getLogger().info(`Pull request already exists: ${existingPR.url}`);
|
|
246
|
+
if (openInBrowser) {
|
|
247
|
+
await this.openPRInBrowser(existingPR.url);
|
|
248
|
+
}
|
|
249
|
+
return {
|
|
250
|
+
url: existingPR.url,
|
|
251
|
+
number: existingPR.number,
|
|
252
|
+
wasExisting: true
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
const body = await this.generatePRBody(issueNumber, worktreePath);
|
|
256
|
+
getLogger().info("Creating pull request...");
|
|
257
|
+
const url = await this.createPR(branchName, title, body, baseBranch, worktreePath);
|
|
258
|
+
const prNumber = this.extractPRNumberFromUrl(url);
|
|
259
|
+
if (openInBrowser) {
|
|
260
|
+
await this.openPRInBrowser(url);
|
|
261
|
+
}
|
|
262
|
+
return {
|
|
263
|
+
url,
|
|
264
|
+
number: prNumber,
|
|
265
|
+
wasExisting: false
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Extract PR number from GitHub PR URL
|
|
270
|
+
* @param url - PR URL (e.g., https://github.com/owner/repo/pull/123)
|
|
271
|
+
* @returns PR number
|
|
272
|
+
*/
|
|
273
|
+
extractPRNumberFromUrl(url) {
|
|
274
|
+
const match = url.match(/\/pull\/(\d+)/);
|
|
275
|
+
if (match == null ? void 0 : match[1]) {
|
|
276
|
+
return parseInt(match[1], 10);
|
|
277
|
+
}
|
|
278
|
+
throw new Error(`Could not extract PR number from URL: ${url}`);
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Create a draft PR for the branch
|
|
282
|
+
* Used by github-draft-pr mode during il start
|
|
283
|
+
* @param branchName - Branch to create PR from (used as --head)
|
|
284
|
+
* @param title - PR title
|
|
285
|
+
* @param body - PR body
|
|
286
|
+
* @param baseBranch - Base branch to target (usually main/master)
|
|
287
|
+
* @param cwd - Working directory
|
|
288
|
+
* @returns PR URL and number
|
|
289
|
+
*/
|
|
290
|
+
async createDraftPR(branchName, title, body, baseBranch, cwd) {
|
|
291
|
+
try {
|
|
292
|
+
const targetRemote = await getEffectivePRTargetRemote(this.settings, cwd);
|
|
293
|
+
let headValue = branchName;
|
|
294
|
+
if (targetRemote !== "origin") {
|
|
295
|
+
const remotes = await parseGitRemotes(cwd);
|
|
296
|
+
const originRemote = remotes.find((r) => r.name === "origin");
|
|
297
|
+
if (originRemote) {
|
|
298
|
+
headValue = `${originRemote.owner}:${branchName}`;
|
|
299
|
+
getLogger().debug(`Fork workflow detected, using head: ${headValue}`);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
const args = ["pr", "create", "--head", headValue, "--title", title, "--body", body, "--base", baseBranch, "--draft"];
|
|
303
|
+
if (targetRemote !== "origin") {
|
|
304
|
+
const repo = await getConfiguredRepoFromSettings(this.settings, cwd);
|
|
305
|
+
args.push("--repo", repo);
|
|
306
|
+
}
|
|
307
|
+
const result = await executeGhCommand(args, cwd ? { cwd } : void 0);
|
|
308
|
+
const url = typeof result === "string" ? result.trim() : String(result).trim();
|
|
309
|
+
if (!url.includes("github.com") || !url.includes("/pull/")) {
|
|
310
|
+
throw new Error(`Unexpected response from gh pr create --draft: ${url}`);
|
|
311
|
+
}
|
|
312
|
+
const number = this.extractPRNumberFromUrl(url);
|
|
313
|
+
return { url, number };
|
|
314
|
+
} catch (error) {
|
|
315
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
316
|
+
if (errorMessage.includes("Head sha can't be blank") || errorMessage.includes("No commits between")) {
|
|
317
|
+
throw new Error(
|
|
318
|
+
`Failed to create draft pull request: ${errorMessage}
|
|
319
|
+
|
|
320
|
+
This error typically occurs when:
|
|
321
|
+
- The branch was not fully pushed to the remote
|
|
322
|
+
- The branch has no commits ahead of the base branch
|
|
323
|
+
|
|
324
|
+
Try running: git push -u origin ${branchName}
|
|
325
|
+
Then retry: il start`
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
throw new Error(`Failed to create draft pull request: ${errorMessage}`);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Mark a draft PR as ready for review
|
|
333
|
+
* Used by github-draft-pr mode during il finish
|
|
334
|
+
* @param prNumber - PR number to mark ready
|
|
335
|
+
* @param cwd - Working directory
|
|
336
|
+
*/
|
|
337
|
+
async markPRReady(prNumber, cwd) {
|
|
338
|
+
const args = ["pr", "ready", String(prNumber)];
|
|
339
|
+
await executeGhCommand(args, cwd ? { cwd } : void 0);
|
|
340
|
+
getLogger().info(`Marked PR #${prNumber} as ready for review`);
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
export {
|
|
345
|
+
PRManager
|
|
346
|
+
};
|
|
347
|
+
//# sourceMappingURL=chunk-P2ZQ5LKB.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/lib/PRManager.ts"],"sourcesContent":["import { executeGhCommand } from '../utils/github.js'\nimport { launchClaude, detectClaudeCli } from '../utils/claude.js'\nimport { getEffectivePRTargetRemote, getConfiguredRepoFromSettings, parseGitRemotes } from '../utils/remote.js'\nimport { openBrowser } from '../utils/browser.js'\nimport { getLogger } from '../utils/logger-context.js'\nimport type { IloomSettings } from './SettingsManager.js'\n\ninterface ExistingPR {\n\tnumber: number\n\turl: string\n}\n\ninterface PRCreationResult {\n\turl: string\n\tnumber: number\n\twasExisting: boolean\n}\n\nexport class PRManager {\n\tconstructor(private settings: IloomSettings) {\n\t\t// Uses getLogger() for all logging operations\n\t}\n\n\t/**\n\t * Check if a PR already exists for the given branch\n\t * @param branchName - Branch to check\n\t * @param cwd - Working directory\n\t * @returns Existing PR info or null if none found\n\t */\n\tasync checkForExistingPR(branchName: string, cwd?: string): Promise<ExistingPR | null> {\n\t\ttry {\n\t\t\tconst prList = await executeGhCommand<Array<{ number: number; url: string }>>(\n\t\t\t\t['pr', 'list', '--head', branchName, '--state', 'open', '--json', 'number,url'],\n\t\t\t\tcwd ? { cwd } : undefined\n\t\t\t)\n\n\t\t\tif (prList.length > 0) {\n\t\t\t\treturn prList[0] ?? null // Return first match\n\t\t\t}\n\n\t\t\treturn null\n\t\t} catch (error) {\n\t\tgetLogger().debug('Error checking for existing PR', { error })\n\t\t\treturn null\n\t\t}\n\t}\n\n\t/**\n\t * Generate PR body using Claude if available, otherwise use simple template\n\t * @param issueNumber - Issue number to include in body\n\t * @param worktreePath - Path to worktree for context\n\t * @returns PR body markdown\n\t */\n\tasync generatePRBody(issueNumber: string | number | undefined, worktreePath: string): Promise<string> {\n\t\t// Try Claude first for rich body generation\n\t\tconst hasClaudeCli = await detectClaudeCli()\n\n\t\tif (hasClaudeCli) {\n\t\t\ttry {\n\t\t\t\tconst prompt = this.buildPRBodyPrompt(issueNumber)\n\n\t\t\t\tconst body = await launchClaude(prompt, {\n\t\t\t\t\theadless: true,\n\t\t\t\t\taddDir: worktreePath,\n\t\t\t\t\ttimeout: 30000,\n\t\t\t\t})\n\n\t\t\t\tif (body && typeof body === 'string' && body.trim()) {\n\t\t\t\t\tconst sanitized = this.sanitizeClaudeOutput(body)\n\t\t\t\t\tif (sanitized) {\n\t\t\t\t\t\treturn sanitized\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\tgetLogger().debug('Claude PR body generation failed, using template', { error })\n\t\t\t}\n\t\t}\n\n\t\t// Fallback to simple template\n\t\tlet body = 'This PR contains changes from the iloom workflow.\\n\\n'\n\n\t\tif (issueNumber) {\n\t\t\tbody += `Fixes #${issueNumber}`\n\t\t}\n\n\t\treturn body\n\t}\n\n\t/**\n\t * Build structured XML prompt for PR body generation\n\t * Uses XML format for clear task definition and output expectations\n\t */\n\tprivate buildPRBodyPrompt(issueNumber?: string | number): string {\n\t\tconst issueContext = issueNumber\n\t\t\t? `\\n<IssueContext>\nThis PR is associated with GitHub issue #${issueNumber}.\nInclude \"Fixes #${issueNumber}\" at the end of the body on its own line.\n</IssueContext>`\n\t\t\t: ''\n\n\t\treturn `<Task>\nYou are a software engineer writing a pull request body for this repository.\nExamine the changes in the git repository and generate a concise, professional PR description.\n</Task>\n\n<Requirements>\n<Format>Write 2-3 sentences summarizing what was changed and why.${issueNumber ? `\\n\\nEnd with \"Fixes #${issueNumber}\" on its own line.` : ''}</Format>\n<Tone>Professional and concise</Tone>\n<Focus>Summarize the changes and their purpose</Focus>\n<NoMeta>CRITICAL: Do NOT include ANY explanatory text, analysis, or meta-commentary. Output ONLY the raw PR body text.</NoMeta>\n<Examples>\nGood: \"Add user authentication with JWT tokens to secure the API endpoints. This includes login and registration endpoints with proper password hashing.\n\nFixes #42\"\nGood: \"Fix navigation bug in sidebar menu that caused incorrect highlighting on nested routes.\"\nBad: \"Here's the PR body:\\n\\n---\\n\\nAdd user authentication...\"\nBad: \"Based on the changes, I'll write: Fix navigation bug...\"\n</Examples>\n${issueContext}\n</Requirements>\n\n<Output>\nIMPORTANT: Your entire response will be used directly as the GitHub pull request body.\nDo not include any explanatory text, headers, or separators before or after the body.\nStart your response immediately with the PR body text.\n</Output>`\n\t}\n\n\t/**\n\t * Sanitize Claude output to remove meta-commentary and clean formatting\n\t * Handles cases where Claude includes explanatory text despite instructions\n\t */\n\tprivate sanitizeClaudeOutput(rawOutput: string): string {\n\t\tlet cleaned = rawOutput.trim()\n\n\t\t// Remove common meta-commentary patterns (case-insensitive)\n\t\tconst metaPatterns = [\n\t\t\t/^.*?based on.*?changes.*?:/i,\n\t\t\t/^.*?looking at.*?files.*?:/i,\n\t\t\t/^.*?examining.*?:/i,\n\t\t\t/^.*?analyzing.*?:/i,\n\t\t\t/^.*?i'll.*?generate.*?:/i,\n\t\t\t/^.*?let me.*?:/i,\n\t\t\t/^.*?here.*?is.*?(?:the\\s+)?(?:pr|pull request).*?body.*?:/i,\n\t\t\t/^.*?here's.*?(?:the\\s+)?(?:pr|pull request).*?body.*?:/i,\n\t\t]\n\n\t\tfor (const pattern of metaPatterns) {\n\t\t\tcleaned = cleaned.replace(pattern, '').trim()\n\t\t}\n\n\t\t// Remove leading separator lines (---, ===, etc.)\n\t\tcleaned = cleaned.replace(/^[-=]{3,}\\s*/m, '').trim()\n\n\t\t// Extract content after separators only if it looks like meta-commentary\n\t\tif (cleaned.includes(':')) {\n\t\t\tconst colonIndex = cleaned.indexOf(':')\n\t\t\tconst beforeColon = cleaned.substring(0, colonIndex).trim().toLowerCase()\n\n\t\t\t// Only split if the text before colon looks like meta-commentary\n\t\t\tconst metaIndicators = [\n\t\t\t\t'here is the pr body',\n\t\t\t\t'here is the pull request body',\n\t\t\t\t'pr body',\n\t\t\t\t'pull request body',\n\t\t\t\t'here is',\n\t\t\t\t\"here's\",\n\t\t\t\t'the body should be',\n\t\t\t\t'i suggest',\n\t\t\t\t'my suggestion'\n\t\t\t]\n\n\t\t\tconst isMetaCommentary = metaIndicators.some(indicator => beforeColon.includes(indicator))\n\n\t\t\tif (isMetaCommentary) {\n\t\t\t\tconst afterColon = cleaned.substring(colonIndex + 1).trim()\n\t\t\t\t// Remove leading separator after colon\n\t\t\t\tconst afterSeparator = afterColon.replace(/^[-=]{3,}\\s*/m, '').trim()\n\t\t\t\tif (afterSeparator && afterSeparator.length > 10) {\n\t\t\t\t\tcleaned = afterSeparator\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Remove quotes if the entire message is wrapped in them\n\t\tif ((cleaned.startsWith('\"') && cleaned.endsWith('\"')) ||\n\t\t\t(cleaned.startsWith(\"'\") && cleaned.endsWith(\"'\"))) {\n\t\t\tcleaned = cleaned.slice(1, -1).trim()\n\t\t}\n\n\t\treturn cleaned\n\t}\n\n\t/**\n\t * Create a GitHub PR for the branch\n\t * @param branchName - Branch to create PR from (used as --head)\n\t * @param title - PR title\n\t * @param body - PR body\n\t * @param baseBranch - Base branch to target (usually main/master)\n\t * @param cwd - Working directory\n\t * @returns PR URL\n\t */\n\tasync createPR(\n\t\tbranchName: string,\n\t\ttitle: string,\n\t\tbody: string,\n\t\tbaseBranch: string,\n\t\tcwd?: string\n\t): Promise<string> {\n\t\ttry {\n\t\t\t// Get the target remote for the PR\n\t\t\tconst targetRemote = await getEffectivePRTargetRemote(this.settings, cwd)\n\n\t\t\t// Determine the correct --head value\n\t\t\t// For fork workflows (target != origin), we need \"username:branch\" format\n\t\t\t// See: https://github.com/cli/cli/issues/2691\n\t\t\tlet headValue = branchName\n\n\t\t\tif (targetRemote !== 'origin') {\n\t\t\t\t// Fork workflow: need to specify the head as \"owner:branch\"\n\t\t\t\t// Get the owner of the origin remote (where we pushed the branch)\n\t\t\t\tconst remotes = await parseGitRemotes(cwd)\n\t\t\t\tconst originRemote = remotes.find(r => r.name === 'origin')\n\n\t\t\t\tif (originRemote) {\n\t\t\t\t\theadValue = `${originRemote.owner}:${branchName}`\n\t\t\t\tgetLogger().debug(`Fork workflow detected, using head: ${headValue}`)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Build gh pr create command\n\t\t\t// Note: gh pr create returns a plain URL string, not JSON\n\t\t\tconst args = ['pr', 'create', '--head', headValue, '--title', title, '--body', body, '--base', baseBranch]\n\n\t\t\t// If target remote is not 'origin', we need to specify the repo\n\t\t\tif (targetRemote !== 'origin') {\n\t\t\t\tconst repo = await getConfiguredRepoFromSettings(this.settings, cwd)\n\t\t\t\targs.push('--repo', repo)\n\t\t\t}\n\n\t\t\t// gh pr create returns the PR URL as plain text (not JSON)\n\t\t\tconst result = await executeGhCommand<string>(args, cwd ? { cwd } : undefined)\n\n\t\t\t// Result is a string URL like \"https://github.com/owner/repo/pull/123\"\n\t\t\tconst url = typeof result === 'string' ? result.trim() : String(result).trim()\n\n\t\t\tif (!url.includes('github.com') || !url.includes('/pull/')) {\n\t\t\t\tthrow new Error(`Unexpected response from gh pr create: ${url}`)\n\t\t\t}\n\n\t\t\treturn url\n\t\t} catch (error) {\n\t\t\tconst errorMessage = error instanceof Error ? error.message : String(error)\n\n\t\t\t// Provide helpful error message for common GraphQL errors\n\t\t\tif (errorMessage.includes(\"Head sha can't be blank\") || errorMessage.includes(\"No commits between\")) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`Failed to create pull request: ${errorMessage}\\n\\n` +\n\t\t\t\t\t`This error typically occurs when:\\n` +\n\t\t\t\t\t` - The branch was not fully pushed to the remote\\n` +\n\t\t\t\t\t` - There's a race condition between push and PR creation\\n` +\n\t\t\t\t\t` - The branch has no commits ahead of the base branch\\n\\n` +\n\t\t\t\t\t`Try running: git push -u origin ${branchName}\\n` +\n\t\t\t\t\t`Then retry: il finish`\n\t\t\t\t)\n\t\t\t}\n\n\t\t\tthrow new Error(`Failed to create pull request: ${errorMessage}`)\n\t\t}\n\t}\n\n\t/**\n\t * Open PR URL in browser\n\t * @param url - PR URL to open\n\t */\n\tasync openPRInBrowser(url: string): Promise<void> {\n\t\ttry {\n\t\t\tawait openBrowser(url)\n\t\tgetLogger().debug('Opened PR in browser', { url })\n\t\t} catch (error) {\n\t\t\t// Don't fail the whole operation if browser opening fails\n\t\tgetLogger().warn('Failed to open PR in browser', { error })\n\t\t}\n\t}\n\n\t/**\n\t * Complete PR workflow: check for existing, create if needed, optionally open in browser\n\t * @param branchName - Branch to create PR from\n\t * @param title - PR title\n\t * @param issueNumber - Optional issue number for body generation\n\t * @param baseBranch - Base branch to target\n\t * @param worktreePath - Path to worktree\n\t * @param openInBrowser - Whether to open PR in browser\n\t * @returns PR creation result\n\t */\n\tasync createOrOpenPR(\n\t\tbranchName: string,\n\t\ttitle: string,\n\t\tissueNumber: string | number | undefined,\n\t\tbaseBranch: string,\n\t\tworktreePath: string,\n\t\topenInBrowser: boolean\n\t): Promise<PRCreationResult> {\n\t\t// Check for existing PR\n\t\tconst existingPR = await this.checkForExistingPR(branchName, worktreePath)\n\n\t\tif (existingPR) {\n\t\tgetLogger().info(`Pull request already exists: ${existingPR.url}`)\n\n\t\t\tif (openInBrowser) {\n\t\t\t\tawait this.openPRInBrowser(existingPR.url)\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\turl: existingPR.url,\n\t\t\t\tnumber: existingPR.number,\n\t\t\t\twasExisting: true,\n\t\t\t}\n\t\t}\n\n\t\t// Generate PR body\n\t\tconst body = await this.generatePRBody(issueNumber, worktreePath)\n\n\t\t// Create new PR\n\tgetLogger().info('Creating pull request...')\n\t\tconst url = await this.createPR(branchName, title, body, baseBranch, worktreePath)\n\n\t\t// Extract PR number from URL\n\t\tconst prNumber = this.extractPRNumberFromUrl(url)\n\n\t\tif (openInBrowser) {\n\t\t\tawait this.openPRInBrowser(url)\n\t\t}\n\n\t\treturn {\n\t\t\turl,\n\t\t\tnumber: prNumber,\n\t\t\twasExisting: false,\n\t\t}\n\t}\n\n\t/**\n\t * Extract PR number from GitHub PR URL\n\t * @param url - PR URL (e.g., https://github.com/owner/repo/pull/123)\n\t * @returns PR number\n\t */\n\tprivate extractPRNumberFromUrl(url: string): number {\n\t\tconst match = url.match(/\\/pull\\/(\\d+)/)\n\t\tif (match?.[1]) {\n\t\t\treturn parseInt(match[1], 10)\n\t\t}\n\t\tthrow new Error(`Could not extract PR number from URL: ${url}`)\n\t}\n\n\t/**\n\t * Create a draft PR for the branch\n\t * Used by github-draft-pr mode during il start\n\t * @param branchName - Branch to create PR from (used as --head)\n\t * @param title - PR title\n\t * @param body - PR body\n\t * @param baseBranch - Base branch to target (usually main/master)\n\t * @param cwd - Working directory\n\t * @returns PR URL and number\n\t */\n\tasync createDraftPR(\n\t\tbranchName: string,\n\t\ttitle: string,\n\t\tbody: string,\n\t\tbaseBranch: string,\n\t\tcwd?: string\n\t): Promise<{ url: string; number: number }> {\n\t\ttry {\n\t\t\t// Get the target remote for the PR\n\t\t\tconst targetRemote = await getEffectivePRTargetRemote(this.settings, cwd)\n\n\t\t\t// Determine the correct --head value\n\t\t\t// For fork workflows (target != origin), we need \"username:branch\" format\n\t\t\tlet headValue = branchName\n\n\t\t\tif (targetRemote !== 'origin') {\n\t\t\t\t// Fork workflow: need to specify the head as \"owner:branch\"\n\t\t\t\tconst remotes = await parseGitRemotes(cwd)\n\t\t\t\tconst originRemote = remotes.find(r => r.name === 'origin')\n\n\t\t\t\tif (originRemote) {\n\t\t\t\t\theadValue = `${originRemote.owner}:${branchName}`\n\t\t\t\t\tgetLogger().debug(`Fork workflow detected, using head: ${headValue}`)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Build gh pr create command with --draft flag\n\t\t\tconst args = ['pr', 'create', '--head', headValue, '--title', title, '--body', body, '--base', baseBranch, '--draft']\n\n\t\t\t// If target remote is not 'origin', we need to specify the repo\n\t\t\tif (targetRemote !== 'origin') {\n\t\t\t\tconst repo = await getConfiguredRepoFromSettings(this.settings, cwd)\n\t\t\t\targs.push('--repo', repo)\n\t\t\t}\n\n\t\t\t// gh pr create returns the PR URL as plain text (not JSON)\n\t\t\tconst result = await executeGhCommand<string>(args, cwd ? { cwd } : undefined)\n\n\t\t\t// Result is a string URL like \"https://github.com/owner/repo/pull/123\"\n\t\t\tconst url = typeof result === 'string' ? result.trim() : String(result).trim()\n\n\t\t\tif (!url.includes('github.com') || !url.includes('/pull/')) {\n\t\t\t\tthrow new Error(`Unexpected response from gh pr create --draft: ${url}`)\n\t\t\t}\n\n\t\t\tconst number = this.extractPRNumberFromUrl(url)\n\n\t\t\treturn { url, number }\n\t\t} catch (error) {\n\t\t\tconst errorMessage = error instanceof Error ? error.message : String(error)\n\n\t\t\t// Provide helpful error message for common errors\n\t\t\tif (errorMessage.includes(\"Head sha can't be blank\") || errorMessage.includes(\"No commits between\")) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`Failed to create draft pull request: ${errorMessage}\\n\\n` +\n\t\t\t\t\t`This error typically occurs when:\\n` +\n\t\t\t\t\t` - The branch was not fully pushed to the remote\\n` +\n\t\t\t\t\t` - The branch has no commits ahead of the base branch\\n\\n` +\n\t\t\t\t\t`Try running: git push -u origin ${branchName}\\n` +\n\t\t\t\t\t`Then retry: il start`\n\t\t\t\t)\n\t\t\t}\n\n\t\t\tthrow new Error(`Failed to create draft pull request: ${errorMessage}`)\n\t\t}\n\t}\n\n\t/**\n\t * Mark a draft PR as ready for review\n\t * Used by github-draft-pr mode during il finish\n\t * @param prNumber - PR number to mark ready\n\t * @param cwd - Working directory\n\t */\n\tasync markPRReady(prNumber: number, cwd?: string): Promise<void> {\n\t\tconst args = ['pr', 'ready', String(prNumber)]\n\t\tawait executeGhCommand(args, cwd ? { cwd } : undefined)\n\t\tgetLogger().info(`Marked PR #${prNumber} as ready for review`)\n\t}\n}\n"],"mappings":"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","names":["body"]}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
} from "./chunk-
|
|
3
|
+
detectPackageManager
|
|
4
|
+
} from "./chunk-VBFDVGAE.js";
|
|
5
5
|
import {
|
|
6
6
|
ProcessManager
|
|
7
7
|
} from "./chunk-VU3QMIP2.js";
|
|
@@ -12,6 +12,30 @@ import {
|
|
|
12
12
|
// src/lib/DevServerManager.ts
|
|
13
13
|
import { execa } from "execa";
|
|
14
14
|
import { setTimeout } from "timers/promises";
|
|
15
|
+
|
|
16
|
+
// src/utils/dev-server.ts
|
|
17
|
+
async function buildDevServerCommand(workspacePath) {
|
|
18
|
+
const packageManager = await detectPackageManager(workspacePath);
|
|
19
|
+
let devCommand;
|
|
20
|
+
switch (packageManager) {
|
|
21
|
+
case "pnpm":
|
|
22
|
+
devCommand = "pnpm dev";
|
|
23
|
+
break;
|
|
24
|
+
case "npm":
|
|
25
|
+
devCommand = "npm run dev";
|
|
26
|
+
break;
|
|
27
|
+
case "yarn":
|
|
28
|
+
devCommand = "yarn dev";
|
|
29
|
+
break;
|
|
30
|
+
default:
|
|
31
|
+
logger.warn(`Unknown or unsupported package manager: ${packageManager}, defaulting to npm`);
|
|
32
|
+
devCommand = "npm run dev";
|
|
33
|
+
}
|
|
34
|
+
logger.debug(`Dev server command: ${devCommand}`);
|
|
35
|
+
return devCommand;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// src/lib/DevServerManager.ts
|
|
15
39
|
var DevServerManager = class {
|
|
16
40
|
constructor(processManager, options = {}) {
|
|
17
41
|
this.runningServers = /* @__PURE__ */ new Map();
|
|
@@ -119,7 +143,7 @@ var DevServerManager = class {
|
|
|
119
143
|
* @param onProcessStarted - Callback called immediately after process starts with PID
|
|
120
144
|
* @returns Process information including PID
|
|
121
145
|
*/
|
|
122
|
-
async runServerForeground(worktreePath, port, redirectToStderr = false, onProcessStarted) {
|
|
146
|
+
async runServerForeground(worktreePath, port, redirectToStderr = false, onProcessStarted, envOverrides) {
|
|
123
147
|
const devCommand = await buildDevServerCommand(worktreePath);
|
|
124
148
|
logger.debug(`Starting dev server in foreground with command: ${devCommand}`);
|
|
125
149
|
const stdio = redirectToStderr ? [process.stdin, process.stderr, process.stderr] : "inherit";
|
|
@@ -127,7 +151,9 @@ var DevServerManager = class {
|
|
|
127
151
|
cwd: worktreePath,
|
|
128
152
|
env: {
|
|
129
153
|
...process.env,
|
|
154
|
+
...envOverrides,
|
|
130
155
|
PORT: port.toString()
|
|
156
|
+
// PORT always wins (explicit parameter)
|
|
131
157
|
},
|
|
132
158
|
// Configure stdio based on whether we want to redirect output
|
|
133
159
|
stdio
|
|
@@ -161,4 +187,4 @@ var DevServerManager = class {
|
|
|
161
187
|
export {
|
|
162
188
|
DevServerManager
|
|
163
189
|
};
|
|
164
|
-
//# sourceMappingURL=chunk-
|
|
190
|
+
//# sourceMappingURL=chunk-RJKMF6BC.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/lib/DevServerManager.ts","../src/utils/dev-server.ts"],"sourcesContent":["import { execa, type ExecaChildProcess } from 'execa'\nimport { setTimeout } from 'timers/promises'\nimport { ProcessManager } from './process/ProcessManager.js'\nimport { buildDevServerCommand } from '../utils/dev-server.js'\nimport { logger } from '../utils/logger.js'\n\nexport interface DevServerManagerOptions {\n\t/**\n\t * Maximum time to wait for server to start (in milliseconds)\n\t * Default: 30000 (30 seconds)\n\t */\n\tstartupTimeout?: number\n\n\t/**\n\t * Interval between port checks (in milliseconds)\n\t * Default: 1000 (1 second)\n\t */\n\tcheckInterval?: number\n}\n\n/**\n * DevServerManager handles auto-starting and monitoring dev servers\n * Used by open/run commands to ensure dev server is running before opening browser\n */\nexport class DevServerManager {\n\tprivate readonly processManager: ProcessManager\n\tprivate readonly options: Required<DevServerManagerOptions>\n\tprivate runningServers: Map<number, ExecaChildProcess> = new Map()\n\n\tconstructor(\n\t\tprocessManager?: ProcessManager,\n\t\toptions: DevServerManagerOptions = {}\n\t) {\n\t\tthis.processManager = processManager ?? new ProcessManager()\n\t\tthis.options = {\n\t\t\tstartupTimeout: options.startupTimeout ?? 30000,\n\t\t\tcheckInterval: options.checkInterval ?? 1000,\n\t\t}\n\t}\n\n\t/**\n\t * Ensure dev server is running on the specified port\n\t * If not running, start it and wait for it to be ready\n\t *\n\t * @param worktreePath - Path to the worktree\n\t * @param port - Port the server should run on\n\t * @returns true if server is ready, false if startup failed/timed out\n\t */\n\tasync ensureServerRunning(worktreePath: string, port: number): Promise<boolean> {\n\t\tlogger.debug(`Checking if dev server is running on port ${port}...`)\n\n\t\t// Check if already running\n\t\tconst existingProcess = await this.processManager.detectDevServer(port)\n\t\tif (existingProcess) {\n\t\t\tlogger.debug(\n\t\t\t\t`Dev server already running on port ${port} (PID: ${existingProcess.pid})`\n\t\t\t)\n\t\t\treturn true\n\t\t}\n\n\t\t// Not running - start it\n\t\tlogger.info(`Dev server not running on port ${port}, starting...`)\n\n\t\ttry {\n\t\t\tawait this.startDevServer(worktreePath, port)\n\t\t\treturn true\n\t\t} catch (error) {\n\t\t\tlogger.error(\n\t\t\t\t`Failed to start dev server: ${error instanceof Error ? error.message : 'Unknown error'}`\n\t\t\t)\n\t\t\treturn false\n\t\t}\n\t}\n\n\t/**\n\t * Start dev server in background and wait for it to be ready\n\t */\n\tprivate async startDevServer(worktreePath: string, port: number): Promise<void> {\n\t\t// Build dev server command\n\t\tconst devCommand = await buildDevServerCommand(worktreePath)\n\t\tlogger.debug(`Starting dev server with command: ${devCommand}`)\n\n\t\t// Start server in background\n\t\tconst serverProcess = execa('sh', ['-c', devCommand], {\n\t\t\tcwd: worktreePath,\n\t\t\tenv: {\n\t\t\t\t...process.env,\n\t\t\t\tPORT: port.toString(),\n\t\t\t},\n\t\t\t// Important: Don't inherit stdio - server runs in background\n\t\t\tstdio: 'ignore',\n\t\t\t// Detach from parent process so it continues running\n\t\t\tdetached: true,\n\t\t})\n\n\t\t// Store reference to prevent cleanup\n\t\tthis.runningServers.set(port, serverProcess)\n\n\t\t// Unref so parent can exit\n\t\tserverProcess.unref()\n\n\t\t// Wait for server to be ready\n\t\tlogger.info(`Waiting for dev server to start on port ${port}...`)\n\t\tconst ready = await this.waitForServerReady(port)\n\n\t\tif (!ready) {\n\t\t\tthrow new Error(\n\t\t\t\t`Dev server failed to start within ${this.options.startupTimeout}ms timeout`\n\t\t\t)\n\t\t}\n\n\t\tlogger.success(`Dev server started successfully on port ${port}`)\n\t}\n\n\t/**\n\t * Wait for server to be ready by polling the port\n\t */\n\tprivate async waitForServerReady(port: number): Promise<boolean> {\n\t\tconst startTime = Date.now()\n\t\tlet attempts = 0\n\n\t\twhile (Date.now() - startTime < this.options.startupTimeout) {\n\t\t\tattempts++\n\n\t\t\t// Check if server is listening\n\t\t\tconst processInfo = await this.processManager.detectDevServer(port)\n\n\t\t\tif (processInfo) {\n\t\t\t\tlogger.debug(\n\t\t\t\t\t`Server detected on port ${port} after ${attempts} attempts (${Date.now() - startTime}ms)`\n\t\t\t\t)\n\t\t\t\treturn true\n\t\t\t}\n\n\t\t\t// Wait before next check\n\t\t\tawait setTimeout(this.options.checkInterval)\n\t\t}\n\n\t\t// Timeout\n\t\tlogger.warn(\n\t\t\t`Server did not start on port ${port} after ${this.options.startupTimeout}ms (${attempts} attempts)`\n\t\t)\n\t\treturn false\n\t}\n\n\t/**\n\t * Check if a dev server is running on the specified port\n\t *\n\t * @param port - Port to check\n\t * @returns true if server is running, false otherwise\n\t */\n\tasync isServerRunning(port: number): Promise<boolean> {\n\t\tconst existingProcess = await this.processManager.detectDevServer(port)\n\t\treturn existingProcess !== null\n\t}\n\n\t/**\n\t * Run dev server in foreground mode (blocking)\n\t * This method blocks until the server is stopped (e.g., via Ctrl+C)\n\t *\n\t * @param worktreePath - Path to the worktree\n\t * @param port - Port the server should run on\n\t * @param redirectToStderr - If true, redirect stdout/stderr to stderr (useful for JSON output)\n\t * @param onProcessStarted - Callback called immediately after process starts with PID\n\t * @returns Process information including PID\n\t */\n\tasync runServerForeground(\n\t\tworktreePath: string,\n\t\tport: number,\n\t\tredirectToStderr = false,\n\t\tonProcessStarted?: (pid?: number) => void,\n\t\tenvOverrides?: Record<string, string>\n\t): Promise<{ pid?: number }> {\n\t\t// Build dev server command\n\t\tconst devCommand = await buildDevServerCommand(worktreePath)\n\t\tlogger.debug(`Starting dev server in foreground with command: ${devCommand}`)\n\n\t\t// Configure stdio based on redirect option\n\t\tconst stdio = redirectToStderr ? [process.stdin, process.stderr, process.stderr] : 'inherit'\n\n\t\t// Start server in foreground (blocking with configured stdio)\n\t\tconst serverProcess = execa('sh', ['-c', devCommand], {\n\t\t\tcwd: worktreePath,\n\t\t\tenv: {\n\t\t\t\t...process.env,\n\t\t\t\t...envOverrides,\n\t\t\t\tPORT: port.toString(), // PORT always wins (explicit parameter)\n\t\t\t},\n\t\t\t// Configure stdio based on whether we want to redirect output\n\t\t\tstdio,\n\t\t})\n\n\t\t// Process info is available immediately after spawn\n\t\t// Use conditional property to satisfy exactOptionalPropertyTypes\n\t\tconst processInfo: { pid?: number } = serverProcess.pid !== undefined ? { pid: serverProcess.pid } : {}\n\n\t\t// Call the callback immediately with the PID (for JSON output)\n\t\tif (onProcessStarted) {\n\t\t\tonProcessStarted(processInfo.pid)\n\t\t}\n\n\t\t// Now wait for the process to complete (this blocks)\n\t\tawait serverProcess\n\n\t\treturn processInfo\n\t}\n\n\t/**\n\t * Clean up all running server processes\n\t * This should be called when the manager is being disposed\n\t */\n\tasync cleanup(): Promise<void> {\n\t\tfor (const [port, serverProcess] of this.runningServers.entries()) {\n\t\t\ttry {\n\t\t\t\tlogger.debug(`Cleaning up server process on port ${port}`)\n\t\t\t\tserverProcess.kill()\n\t\t\t} catch (error) {\n\t\t\t\tlogger.warn(\n\t\t\t\t\t`Failed to kill server process on port ${port}: ${error instanceof Error ? error.message : 'Unknown error'}`\n\t\t\t\t)\n\t\t\t}\n\t\t}\n\t\tthis.runningServers.clear()\n\t}\n}\n","import { detectPackageManager } from './package-manager.js'\nimport { logger } from './logger.js'\nimport type { Capability } from '../types/loom.js'\n\n/**\n * Build dev server command for workspace\n * Detects package manager and constructs appropriate command\n */\nexport async function buildDevServerCommand(\n\tworkspacePath: string\n): Promise<string> {\n\tconst packageManager = await detectPackageManager(workspacePath)\n\n\tlet devCommand: string\n\n\tswitch (packageManager) {\n\t\tcase 'pnpm':\n\t\t\tdevCommand = 'pnpm dev'\n\t\t\tbreak\n\t\tcase 'npm':\n\t\t\tdevCommand = 'npm run dev'\n\t\t\tbreak\n\t\tcase 'yarn':\n\t\t\tdevCommand = 'yarn dev'\n\t\t\tbreak\n\t\tdefault:\n\t\t\t// Fallback to npm (handles bun and other package managers)\n\t\t\tlogger.warn(`Unknown or unsupported package manager: ${packageManager}, defaulting to npm`)\n\t\t\tdevCommand = 'npm run dev'\n\t}\n\n\tlogger.debug(`Dev server command: ${devCommand}`)\n\treturn devCommand\n}\n\n/**\n * Build complete dev server launch command for terminal\n * Includes VSCode launch, echo message (only for web projects), and dev server start\n */\nexport async function getDevServerLaunchCommand(\n\tworkspacePath: string,\n\tport?: number,\n\tcapabilities: Capability[] = []\n): Promise<string> {\n\tconst devCommand = await buildDevServerCommand(workspacePath)\n\n\tconst commands: string[] = []\n\n\t// // Open VSCode\n\t// commands.push('code .')\n\n\t// Echo message (only for web projects)\n\tif (capabilities.includes('web')) {\n\t\tif (port !== undefined) {\n\t\t\tcommands.push(`echo 'Starting dev server on PORT=${port}...'`)\n\t\t} else {\n\t\t\tcommands.push(`echo 'Starting dev server...'`)\n\t\t}\n\t}\n\n\t// Start dev server\n\tcommands.push(devCommand)\n\n\treturn commands.join(' && ')\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
PromptTemplateManager
|
|
4
|
+
} from "./chunk-DKQ4SUII.js";
|
|
2
5
|
import {
|
|
3
6
|
logger
|
|
4
7
|
} from "./chunk-UYVWLISQ.js";
|
|
@@ -111,7 +114,8 @@ var MarkdownAgentParser = class {
|
|
|
111
114
|
|
|
112
115
|
// src/lib/AgentManager.ts
|
|
113
116
|
var AgentManager = class {
|
|
114
|
-
constructor(agentDir) {
|
|
117
|
+
constructor(agentDir, templateManager) {
|
|
118
|
+
this.templateManager = templateManager ?? new PromptTemplateManager();
|
|
115
119
|
if (agentDir) {
|
|
116
120
|
this.agentDir = agentDir;
|
|
117
121
|
} else {
|
|
@@ -136,11 +140,12 @@ var AgentManager = class {
|
|
|
136
140
|
}
|
|
137
141
|
/**
|
|
138
142
|
* Load all agent configuration files from markdown (.md) format
|
|
139
|
-
* Optionally apply model overrides from settings
|
|
143
|
+
* Optionally apply model overrides from settings and template variable substitution
|
|
140
144
|
* Throws error if agents directory doesn't exist or files are malformed
|
|
141
145
|
* @param settings - Optional project settings with per-agent model overrides
|
|
146
|
+
* @param templateVariables - Optional variables for template substitution in agent prompts
|
|
142
147
|
*/
|
|
143
|
-
async loadAgents(settings) {
|
|
148
|
+
async loadAgents(settings, templateVariables) {
|
|
144
149
|
const { readdir } = await import("fs/promises");
|
|
145
150
|
const files = await readdir(this.agentDir);
|
|
146
151
|
const agentFiles = files.filter((file) => file.endsWith(".md"));
|
|
@@ -162,6 +167,15 @@ var AgentManager = class {
|
|
|
162
167
|
);
|
|
163
168
|
}
|
|
164
169
|
}
|
|
170
|
+
if (templateVariables) {
|
|
171
|
+
for (const [agentName, agentConfig] of Object.entries(agents)) {
|
|
172
|
+
agents[agentName] = {
|
|
173
|
+
...agentConfig,
|
|
174
|
+
prompt: this.templateManager.substituteVariables(agentConfig.prompt, templateVariables)
|
|
175
|
+
};
|
|
176
|
+
logger.debug(`Applied template substitution to agent: ${agentName}`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
165
179
|
if (settings == null ? void 0 : settings.agents) {
|
|
166
180
|
for (const [agentName, agentSettings] of Object.entries(settings.agents)) {
|
|
167
181
|
if (agents[agentName] && agentSettings.model) {
|
|
@@ -245,4 +259,4 @@ var AgentManager = class {
|
|
|
245
259
|
export {
|
|
246
260
|
AgentManager
|
|
247
261
|
};
|
|
248
|
-
//# sourceMappingURL=chunk-
|
|
262
|
+
//# sourceMappingURL=chunk-RNZMHJK7.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/lib/AgentManager.ts","../src/utils/MarkdownAgentParser.ts"],"sourcesContent":["import { readFile } from 'fs/promises'\nimport { accessSync } from 'fs'\nimport path from 'path'\nimport { fileURLToPath } from 'url'\nimport { MarkdownAgentParser } from '../utils/MarkdownAgentParser.js'\nimport { logger } from '../utils/logger.js'\nimport type { IloomSettings } from './SettingsManager.js'\nimport { PromptTemplateManager, TemplateVariables } from './PromptTemplateManager.js'\n\n// Agent schema interface\nexport interface AgentConfig {\n\tdescription: string\n\tprompt: string\n\ttools: string[]\n\tmodel: string\n\tcolor?: string\n}\n\n// Container for all loaded agents (keyed by agent name without extension)\nexport interface AgentConfigs {\n\t[agentName: string]: AgentConfig\n}\n\nexport class AgentManager {\n\tprivate agentDir: string\n\tprivate templateManager: PromptTemplateManager\n\n\tconstructor(agentDir?: string, templateManager?: PromptTemplateManager) {\n\t\tthis.templateManager = templateManager ?? new PromptTemplateManager()\n\t\tif (agentDir) {\n\t\t\tthis.agentDir = agentDir\n\t\t} else {\n\t\t\t// Find agents relative to package installation\n\t\t\t// Same pattern as PromptTemplateManager\n\t\t\t// When running from dist/, agents are copied to dist/agents/\n\t\t\tconst currentFileUrl = import.meta.url\n\t\t\tconst currentFilePath = fileURLToPath(currentFileUrl)\n\t\t\tconst distDir = path.dirname(currentFilePath)\n\n\t\t\t// Walk up to find the agents directory\n\t\t\tlet agentDirPath = path.join(distDir, 'agents')\n\t\t\tlet currentDir = distDir\n\n\t\t\twhile (currentDir !== path.dirname(currentDir)) {\n\t\t\t\tconst candidatePath = path.join(currentDir, 'agents')\n\t\t\t\ttry {\n\t\t\t\t\taccessSync(candidatePath)\n\t\t\t\t\tagentDirPath = candidatePath\n\t\t\t\t\tbreak\n\t\t\t\t} catch {\n\t\t\t\t\tcurrentDir = path.dirname(currentDir)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tthis.agentDir = agentDirPath\n\t\t\tlogger.debug('AgentManager initialized', { agentDir: this.agentDir })\n\t\t}\n\t}\n\n\t/**\n\t * Load all agent configuration files from markdown (.md) format\n\t * Optionally apply model overrides from settings and template variable substitution\n\t * Throws error if agents directory doesn't exist or files are malformed\n\t * @param settings - Optional project settings with per-agent model overrides\n\t * @param templateVariables - Optional variables for template substitution in agent prompts\n\t */\n\tasync loadAgents(settings?: IloomSettings, templateVariables?: TemplateVariables): Promise<AgentConfigs> {\n\t\t// Load all .md files from the agents directory\n\t\tconst { readdir } = await import('fs/promises')\n\t\tconst files = await readdir(this.agentDir)\n\t\tconst agentFiles = files.filter(file => file.endsWith('.md'))\n\n\t\tconst agents: AgentConfigs = {}\n\n\t\tfor (const filename of agentFiles) {\n\t\t\tconst agentPath = path.join(this.agentDir, filename)\n\n\t\t\ttry {\n\t\t\t\tconst content = await readFile(agentPath, 'utf-8')\n\n\t\t\t\t// Parse markdown with frontmatter\n\t\t\t\tconst parsed = this.parseMarkdownAgent(content, filename)\n\t\t\t\tconst agentConfig = parsed.config\n\t\t\t\tconst agentName = parsed.name\n\n\t\t\t\t// Validate required fields\n\t\t\t\tthis.validateAgentConfig(agentConfig, agentName)\n\n\t\t\t\tagents[agentName] = agentConfig\n\t\t\t\tlogger.debug(`Loaded agent: ${agentName}`)\n\t\t\t} catch (error) {\n\t\t\t\tlogger.error(`Failed to load agent from ${filename}`, { error })\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`Failed to load agent from ${filename}: ${error instanceof Error ? error.message : 'Unknown error'}`,\n\t\t\t\t)\n\t\t\t}\n\t\t}\n\n\t\t// Apply template variable substitution to agent prompts if variables provided\n\t\tif (templateVariables) {\n\t\t\tfor (const [agentName, agentConfig] of Object.entries(agents)) {\n\t\t\t\tagents[agentName] = {\n\t\t\t\t\t...agentConfig,\n\t\t\t\t\tprompt: this.templateManager.substituteVariables(agentConfig.prompt, templateVariables),\n\t\t\t\t}\n\t\t\t\tlogger.debug(`Applied template substitution to agent: ${agentName}`)\n\t\t\t}\n\t\t}\n\n\t\t// Apply settings overrides if provided\n\t\tif (settings?.agents) {\n\t\t\tfor (const [agentName, agentSettings] of Object.entries(settings.agents)) {\n\t\t\t\tif (agents[agentName] && agentSettings.model) {\n\t\t\t\t\tlogger.debug(`Overriding model for ${agentName}: ${agents[agentName].model} -> ${agentSettings.model}`)\n\t\t\t\t\tagents[agentName] = {\n\t\t\t\t\t\t...agents[agentName],\n\t\t\t\t\t\tmodel: agentSettings.model,\n\t\t\t\t\t}\n\t\t\t\t} else if (!agents[agentName]) {\n\t\t\t\t\tlogger.warn(`Settings reference unknown agent: ${agentName}`)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn agents\n\t}\n\n\t/**\n\t * Validate agent configuration has required fields\n\t */\n\tprivate validateAgentConfig(config: AgentConfig, agentName: string): void {\n\t\tconst requiredFields: (keyof AgentConfig)[] = ['description', 'prompt', 'tools', 'model']\n\n\t\tfor (const field of requiredFields) {\n\t\t\tif (!config[field]) {\n\t\t\t\tthrow new Error(`Agent ${agentName} missing required field: ${field}`)\n\t\t\t}\n\t\t}\n\n\t\tif (!Array.isArray(config.tools)) {\n\t\t\tthrow new Error(`Agent ${agentName} tools must be an array`)\n\t\t}\n\t}\n\n\t/**\n\t * Parse markdown agent file with YAML frontmatter\n\t * @param content - Raw markdown file content\n\t * @param filename - Original filename for error messages\n\t * @returns Parsed agent config and name\n\t */\n\tprivate parseMarkdownAgent(content: string, filename: string): { config: AgentConfig; name: string } {\n\t\ttry {\n\t\t\t// Parse frontmatter using custom parser\n\t\t\tconst { data, content: markdownBody } = MarkdownAgentParser.parse(content)\n\n\t\t\t// Validate frontmatter has required fields\n\t\t\tif (!data.name) {\n\t\t\t\tthrow new Error('Missing required field: name')\n\t\t\t}\n\t\t\tif (!data.description) {\n\t\t\t\tthrow new Error('Missing required field: description')\n\t\t\t}\n\t\t\tif (!data.tools) {\n\t\t\t\tthrow new Error('Missing required field: tools')\n\t\t\t}\n\t\t\tif (!data.model) {\n\t\t\t\tthrow new Error('Missing required field: model')\n\t\t\t}\n\n\t\t\t// Parse tools from comma-separated string to array\n\t\t\tconst tools = data.tools\n\t\t\t\t.split(',')\n\t\t\t\t.map((tool: string) => tool.trim())\n\t\t\t\t.filter((tool: string) => tool.length > 0)\n\n\t\t\t// Validate model and warn if non-standard\n\t\t\tconst validModels = ['sonnet', 'opus', 'haiku']\n\t\t\tif (!validModels.includes(data.model)) {\n\t\t\t\tlogger.warn(\n\t\t\t\t\t`Agent ${data.name} uses model \"${data.model}\" which may not be recognized by Claude CLI, and your workflow may fail or produce unexpected results. ` +\n\t\t\t\t\t\t`Valid values are: ${validModels.join(', ')}`\n\t\t\t\t)\n\t\t\t}\n\n\t\t\t// Construct AgentConfig\n\t\t\tconst config: AgentConfig = {\n\t\t\t\tdescription: data.description,\n\t\t\t\tprompt: markdownBody.trim(),\n\t\t\t\ttools,\n\t\t\t\tmodel: data.model,\n\t\t\t\t...(data.color && { color: data.color }),\n\t\t\t}\n\n\t\t\treturn { config, name: data.name }\n\t\t} catch (error) {\n\t\t\tthrow new Error(\n\t\t\t\t`Failed to parse markdown agent ${filename}: ${error instanceof Error ? error.message : 'Unknown error'}`\n\t\t\t)\n\t\t}\n\t}\n\n\t/**\n\t * Format loaded agents for Claude CLI --agents flag\n\t * Returns object suitable for JSON.stringify\n\t */\n\tformatForCli(agents: AgentConfigs): Record<string, unknown> {\n\t\t// The agents object is already in the correct format\n\t\t// Just return it - launchClaude will JSON.stringify it\n\t\treturn agents as Record<string, unknown>\n\t}\n}\n","/**\n * Custom YAML frontmatter parser for agent markdown files\n * Replaces gray-matter dependency with lightweight custom implementation\n */\n\ninterface ParseResult {\n\tdata: Record<string, string>\n\tcontent: string\n}\n\nexport class MarkdownAgentParser {\n\t/**\n\t * Parse markdown content with YAML frontmatter\n\t * @param content - Raw markdown file content\n\t * @returns Object with parsed frontmatter data and markdown body content\n\t * @throws Error if frontmatter is malformed or missing\n\t */\n\tstatic parse(content: string): ParseResult {\n\t\tconst lines = content.split('\\n')\n\n\t\t// Check for opening frontmatter delimiter\n\t\tif (lines[0]?.trim() !== '---') {\n\t\t\tthrow new Error('Missing opening frontmatter delimiter (---)')\n\t\t}\n\n\t\t// Find closing frontmatter delimiter\n\t\tlet closingDelimiterIndex = -1\n\t\tfor (let i = 1; i < lines.length; i++) {\n\t\t\tif (lines[i]?.trim() === '---') {\n\t\t\t\tclosingDelimiterIndex = i\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\tif (closingDelimiterIndex === -1) {\n\t\t\tthrow new Error('Missing closing frontmatter delimiter (---)')\n\t\t}\n\n\t\t// Extract frontmatter lines (between the delimiters)\n\t\tconst frontmatterLines = lines.slice(1, closingDelimiterIndex)\n\n\t\t// Extract markdown body (after closing delimiter)\n\t\tconst bodyLines = lines.slice(closingDelimiterIndex + 1)\n\t\tconst markdownBody = bodyLines.join('\\n')\n\n\t\t// Parse YAML frontmatter into key-value pairs\n\t\tconst data = this.parseYaml(frontmatterLines.join('\\n'))\n\n\t\treturn {\n\t\t\tdata,\n\t\t\tcontent: markdownBody,\n\t\t}\n\t}\n\n\t/**\n\t * Parse simplified YAML into key-value object\n\t * Supports:\n\t * - Simple key: value pairs\n\t * - Multiline values with | indicator\n\t * - Values with special characters and newlines\n\t *\n\t * @param yaml - YAML string to parse\n\t * @returns Object with parsed key-value pairs\n\t */\n\tprivate static parseYaml(yaml: string): Record<string, string> {\n\t\tconst result: Record<string, string> = {}\n\t\tconst lines = yaml.split('\\n')\n\t\tlet currentKey: string | null = null\n\t\tlet currentValue: string[] = []\n\t\tlet isMultiline = false\n\n\t\tconst finalizeCurrent = (): void => {\n\t\t\tif (currentKey && currentValue.length > 0) {\n\t\t\t\tresult[currentKey] = currentValue.join('\\n').trim()\n\t\t\t\tcurrentKey = null\n\t\t\t\tcurrentValue = []\n\t\t\t}\n\t\t}\n\n\t\tfor (const line of lines) {\n\t\t\t// Skip empty lines when not in multiline mode\n\t\t\tif (!isMultiline && line.trim() === '') {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// Check if this is a new key-value pair\n\t\t\tconst keyValueMatch = line.match(/^([a-zA-Z_][a-zA-Z0-9_-]*)\\s*:\\s*(.*)$/)\n\n\t\t\tif (keyValueMatch && !isMultiline) {\n\t\t\t\t// Finalize previous key if exists\n\t\t\t\tfinalizeCurrent()\n\n\t\t\t\tconst [, key, value] = keyValueMatch\n\t\t\t\tif (!key || value === undefined) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tcurrentKey = key\n\n\t\t\t\t// Check for multiline indicator\n\t\t\t\tif (value.trim() === '|') {\n\t\t\t\t\tisMultiline = true\n\t\t\t\t\tcurrentValue = []\n\t\t\t\t} else {\n\t\t\t\t\t// Single line value\n\t\t\t\t\tcurrentValue = [value]\n\t\t\t\t\tfinalizeCurrent()\n\t\t\t\t\tisMultiline = false\n\t\t\t\t}\n\t\t\t} else if (isMultiline && currentKey) {\n\t\t\t\t// Continuation of multiline value\n\t\t\t\t// Check if we've returned to normal indentation (new key)\n\t\t\t\tif (line.match(/^[a-zA-Z_][a-zA-Z0-9_-]*\\s*:/) && !line.startsWith(' ')) {\n\t\t\t\t\t// End of multiline, this is a new key\n\t\t\t\t\tfinalizeCurrent()\n\t\t\t\t\tisMultiline = false\n\n\t\t\t\t\t// Process this line as a new key\n\t\t\t\t\tconst match = line.match(/^([a-zA-Z_][a-zA-Z0-9_-]*)\\s*:\\s*(.*)$/)\n\t\t\t\t\tif (match) {\n\t\t\t\t\t\tconst [, key, value] = match\n\t\t\t\t\t\tif (key && value !== undefined) {\n\t\t\t\t\t\t\tcurrentKey = key\n\t\t\t\t\t\t\tcurrentValue = [value]\n\t\t\t\t\t\t\tfinalizeCurrent()\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// Remove leading spaces (common indentation) from multiline values\n\t\t\t\t\tconst trimmedLine = line.replace(/^ {2}/, '')\n\t\t\t\t\tcurrentValue.push(trimmedLine)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Finalize last key\n\t\tfinalizeCurrent()\n\n\t\treturn result\n\t}\n}\n"],"mappings":";;;;;;;;;AAAA,SAAS,gBAAgB;AACzB,SAAS,kBAAkB;AAC3B,OAAO,UAAU;AACjB,SAAS,qBAAqB;;;ACOvB,IAAM,sBAAN,MAA0B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOhC,OAAO,MAAM,SAA8B;AAjB5C;AAkBE,UAAM,QAAQ,QAAQ,MAAM,IAAI;AAGhC,UAAI,WAAM,CAAC,MAAP,mBAAU,YAAW,OAAO;AAC/B,YAAM,IAAI,MAAM,6CAA6C;AAAA,IAC9D;AAGA,QAAI,wBAAwB;AAC5B,aAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;AACtC,YAAI,WAAM,CAAC,MAAP,mBAAU,YAAW,OAAO;AAC/B,gCAAwB;AACxB;AAAA,MACD;AAAA,IACD;AAEA,QAAI,0BAA0B,IAAI;AACjC,YAAM,IAAI,MAAM,6CAA6C;AAAA,IAC9D;AAGA,UAAM,mBAAmB,MAAM,MAAM,GAAG,qBAAqB;AAG7D,UAAM,YAAY,MAAM,MAAM,wBAAwB,CAAC;AACvD,UAAM,eAAe,UAAU,KAAK,IAAI;AAGxC,UAAM,OAAO,KAAK,UAAU,iBAAiB,KAAK,IAAI,CAAC;AAEvD,WAAO;AAAA,MACN;AAAA,MACA,SAAS;AAAA,IACV;AAAA,EACD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYA,OAAe,UAAU,MAAsC;AAC9D,UAAM,SAAiC,CAAC;AACxC,UAAM,QAAQ,KAAK,MAAM,IAAI;AAC7B,QAAI,aAA4B;AAChC,QAAI,eAAyB,CAAC;AAC9B,QAAI,cAAc;AAElB,UAAM,kBAAkB,MAAY;AACnC,UAAI,cAAc,aAAa,SAAS,GAAG;AAC1C,eAAO,UAAU,IAAI,aAAa,KAAK,IAAI,EAAE,KAAK;AAClD,qBAAa;AACb,uBAAe,CAAC;AAAA,MACjB;AAAA,IACD;AAEA,eAAW,QAAQ,OAAO;AAEzB,UAAI,CAAC,eAAe,KAAK,KAAK,MAAM,IAAI;AACvC;AAAA,MACD;AAGA,YAAM,gBAAgB,KAAK,MAAM,wCAAwC;AAEzE,UAAI,iBAAiB,CAAC,aAAa;AAElC,wBAAgB;AAEhB,cAAM,CAAC,EAAE,KAAK,KAAK,IAAI;AACvB,YAAI,CAAC,OAAO,UAAU,QAAW;AAChC;AAAA,QACD;AACA,qBAAa;AAGb,YAAI,MAAM,KAAK,MAAM,KAAK;AACzB,wBAAc;AACd,yBAAe,CAAC;AAAA,QACjB,OAAO;AAEN,yBAAe,CAAC,KAAK;AACrB,0BAAgB;AAChB,wBAAc;AAAA,QACf;AAAA,MACD,WAAW,eAAe,YAAY;AAGrC,YAAI,KAAK,MAAM,8BAA8B,KAAK,CAAC,KAAK,WAAW,GAAG,GAAG;AAExE,0BAAgB;AAChB,wBAAc;AAGd,gBAAM,QAAQ,KAAK,MAAM,wCAAwC;AACjE,cAAI,OAAO;AACV,kBAAM,CAAC,EAAE,KAAK,KAAK,IAAI;AACvB,gBAAI,OAAO,UAAU,QAAW;AAC/B,2BAAa;AACb,6BAAe,CAAC,KAAK;AACrB,8BAAgB;AAAA,YACjB;AAAA,UACD;AAAA,QACD,OAAO;AAEN,gBAAM,cAAc,KAAK,QAAQ,SAAS,EAAE;AAC5C,uBAAa,KAAK,WAAW;AAAA,QAC9B;AAAA,MACD;AAAA,IACD;AAGA,oBAAgB;AAEhB,WAAO;AAAA,EACR;AACD;;;ADpHO,IAAM,eAAN,MAAmB;AAAA,EAIzB,YAAY,UAAmB,iBAAyC;AACvE,SAAK,kBAAkB,mBAAmB,IAAI,sBAAsB;AACpE,QAAI,UAAU;AACb,WAAK,WAAW;AAAA,IACjB,OAAO;AAIN,YAAM,iBAAiB,YAAY;AACnC,YAAM,kBAAkB,cAAc,cAAc;AACpD,YAAM,UAAU,KAAK,QAAQ,eAAe;AAG5C,UAAI,eAAe,KAAK,KAAK,SAAS,QAAQ;AAC9C,UAAI,aAAa;AAEjB,aAAO,eAAe,KAAK,QAAQ,UAAU,GAAG;AAC/C,cAAM,gBAAgB,KAAK,KAAK,YAAY,QAAQ;AACpD,YAAI;AACH,qBAAW,aAAa;AACxB,yBAAe;AACf;AAAA,QACD,QAAQ;AACP,uBAAa,KAAK,QAAQ,UAAU;AAAA,QACrC;AAAA,MACD;AAEA,WAAK,WAAW;AAChB,aAAO,MAAM,4BAA4B,EAAE,UAAU,KAAK,SAAS,CAAC;AAAA,IACrE;AAAA,EACD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAM,WAAW,UAA0B,mBAA8D;AAExG,UAAM,EAAE,QAAQ,IAAI,MAAM,OAAO,aAAa;AAC9C,UAAM,QAAQ,MAAM,QAAQ,KAAK,QAAQ;AACzC,UAAM,aAAa,MAAM,OAAO,UAAQ,KAAK,SAAS,KAAK,CAAC;AAE5D,UAAM,SAAuB,CAAC;AAE9B,eAAW,YAAY,YAAY;AAClC,YAAM,YAAY,KAAK,KAAK,KAAK,UAAU,QAAQ;AAEnD,UAAI;AACH,cAAM,UAAU,MAAM,SAAS,WAAW,OAAO;AAGjD,cAAM,SAAS,KAAK,mBAAmB,SAAS,QAAQ;AACxD,cAAM,cAAc,OAAO;AAC3B,cAAM,YAAY,OAAO;AAGzB,aAAK,oBAAoB,aAAa,SAAS;AAE/C,eAAO,SAAS,IAAI;AACpB,eAAO,MAAM,iBAAiB,SAAS,EAAE;AAAA,MAC1C,SAAS,OAAO;AACf,eAAO,MAAM,6BAA6B,QAAQ,IAAI,EAAE,MAAM,CAAC;AAC/D,cAAM,IAAI;AAAA,UACT,6BAA6B,QAAQ,KAAK,iBAAiB,QAAQ,MAAM,UAAU,eAAe;AAAA,QACnG;AAAA,MACD;AAAA,IACD;AAGA,QAAI,mBAAmB;AACtB,iBAAW,CAAC,WAAW,WAAW,KAAK,OAAO,QAAQ,MAAM,GAAG;AAC9D,eAAO,SAAS,IAAI;AAAA,UACnB,GAAG;AAAA,UACH,QAAQ,KAAK,gBAAgB,oBAAoB,YAAY,QAAQ,iBAAiB;AAAA,QACvF;AACA,eAAO,MAAM,2CAA2C,SAAS,EAAE;AAAA,MACpE;AAAA,IACD;AAGA,QAAI,qCAAU,QAAQ;AACrB,iBAAW,CAAC,WAAW,aAAa,KAAK,OAAO,QAAQ,SAAS,MAAM,GAAG;AACzE,YAAI,OAAO,SAAS,KAAK,cAAc,OAAO;AAC7C,iBAAO,MAAM,wBAAwB,SAAS,KAAK,OAAO,SAAS,EAAE,KAAK,OAAO,cAAc,KAAK,EAAE;AACtG,iBAAO,SAAS,IAAI;AAAA,YACnB,GAAG,OAAO,SAAS;AAAA,YACnB,OAAO,cAAc;AAAA,UACtB;AAAA,QACD,WAAW,CAAC,OAAO,SAAS,GAAG;AAC9B,iBAAO,KAAK,qCAAqC,SAAS,EAAE;AAAA,QAC7D;AAAA,MACD;AAAA,IACD;AAEA,WAAO;AAAA,EACR;AAAA;AAAA;AAAA;AAAA,EAKQ,oBAAoB,QAAqB,WAAyB;AACzE,UAAM,iBAAwC,CAAC,eAAe,UAAU,SAAS,OAAO;AAExF,eAAW,SAAS,gBAAgB;AACnC,UAAI,CAAC,OAAO,KAAK,GAAG;AACnB,cAAM,IAAI,MAAM,SAAS,SAAS,4BAA4B,KAAK,EAAE;AAAA,MACtE;AAAA,IACD;AAEA,QAAI,CAAC,MAAM,QAAQ,OAAO,KAAK,GAAG;AACjC,YAAM,IAAI,MAAM,SAAS,SAAS,yBAAyB;AAAA,IAC5D;AAAA,EACD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQQ,mBAAmB,SAAiB,UAAyD;AACpG,QAAI;AAEH,YAAM,EAAE,MAAM,SAAS,aAAa,IAAI,oBAAoB,MAAM,OAAO;AAGzE,UAAI,CAAC,KAAK,MAAM;AACf,cAAM,IAAI,MAAM,8BAA8B;AAAA,MAC/C;AACA,UAAI,CAAC,KAAK,aAAa;AACtB,cAAM,IAAI,MAAM,qCAAqC;AAAA,MACtD;AACA,UAAI,CAAC,KAAK,OAAO;AAChB,cAAM,IAAI,MAAM,+BAA+B;AAAA,MAChD;AACA,UAAI,CAAC,KAAK,OAAO;AAChB,cAAM,IAAI,MAAM,+BAA+B;AAAA,MAChD;AAGA,YAAM,QAAQ,KAAK,MACjB,MAAM,GAAG,EACT,IAAI,CAAC,SAAiB,KAAK,KAAK,CAAC,EACjC,OAAO,CAAC,SAAiB,KAAK,SAAS,CAAC;AAG1C,YAAM,cAAc,CAAC,UAAU,QAAQ,OAAO;AAC9C,UAAI,CAAC,YAAY,SAAS,KAAK,KAAK,GAAG;AACtC,eAAO;AAAA,UACN,SAAS,KAAK,IAAI,gBAAgB,KAAK,KAAK,4HACtB,YAAY,KAAK,IAAI,CAAC;AAAA,QAC7C;AAAA,MACD;AAGA,YAAM,SAAsB;AAAA,QAC3B,aAAa,KAAK;AAAA,QAClB,QAAQ,aAAa,KAAK;AAAA,QAC1B;AAAA,QACA,OAAO,KAAK;AAAA,QACZ,GAAI,KAAK,SAAS,EAAE,OAAO,KAAK,MAAM;AAAA,MACvC;AAEA,aAAO,EAAE,QAAQ,MAAM,KAAK,KAAK;AAAA,IAClC,SAAS,OAAO;AACf,YAAM,IAAI;AAAA,QACT,kCAAkC,QAAQ,KAAK,iBAAiB,QAAQ,MAAM,UAAU,eAAe;AAAA,MACxG;AAAA,IACD;AAAA,EACD;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,aAAa,QAA+C;AAG3D,WAAO;AAAA,EACR;AACD;","names":[]}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
ClaudeService
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-CDQEK2WD.js";
|
|
5
5
|
import {
|
|
6
6
|
logger
|
|
7
7
|
} from "./chunk-UYVWLISQ.js";
|
|
@@ -63,4 +63,4 @@ var ClaudeContextManager = class {
|
|
|
63
63
|
export {
|
|
64
64
|
ClaudeContextManager
|
|
65
65
|
};
|
|
66
|
-
//# sourceMappingURL=chunk-
|
|
66
|
+
//# sourceMappingURL=chunk-S65T4O6I.js.map
|