@iloom/cli 0.4.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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-4UG2E4CD.js} +19 -24
- package/dist/LoomLauncher-4UG2E4CD.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-55TB3FSG.js → chunk-AS2IRKLU.js} +2 -2
- 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-HD5SUKI2.js → chunk-RFUOIUQF.js} +49 -6
- package/dist/{chunk-HD5SUKI2.js.map → chunk-RFUOIUQF.js.map} +1 -1
- 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-IARWMDAX.js → chunk-T5IIUG4Z.js} +98 -16
- package/dist/chunk-T5IIUG4Z.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-MIDJVSIU.js} +14 -14
- package/dist/cli.js +228 -354
- 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-RVIGHBJG.js} +5 -4
- package/dist/{feedback-7PVBQNLJ.js.map → feedback-RVIGHBJG.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-XJALWFAT.js} +57 -22
- package/dist/ignite-XJALWFAT.js.map +1 -0
- package/dist/index.d.ts +58 -7
- package/dist/index.js +104 -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-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-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-MIDJVSIU.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":";;;;;;;;;;;;;;;;;;;;;AAkBO,IAAM,YAAN,MAAgB;AAAA,EACtB,YAAoB,UAAyB;AAAzB;AAAA,EAEpB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,mBAAmB,YAAoB,KAA0C;AACtF,QAAI;AACH,YAAM,SAAS,MAAM;AAAA,QACpB,CAAC,MAAM,QAAQ,UAAU,YAAY,WAAW,QAAQ,UAAU,YAAY;AAAA,QAC9E,MAAM,EAAE,IAAI,IAAI;AAAA,MACjB;AAEA,UAAI,OAAO,SAAS,GAAG;AACtB,eAAO,OAAO,CAAC,KAAK;AAAA,MACrB;AAEA,aAAO;AAAA,IACR,SAAS,OAAO;AAChB,gBAAU,EAAE,MAAM,kCAAkC,EAAE,MAAM,CAAC;AAC5D,aAAO;AAAA,IACR;AAAA,EACD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,eAAe,aAA0C,cAAuC;AAErG,UAAM,eAAe,MAAM,gBAAgB;AAE3C,QAAI,cAAc;AACjB,UAAI;AACH,cAAM,SAAS,KAAK,kBAAkB,WAAW;AAEjD,cAAMA,QAAO,MAAM,aAAa,QAAQ;AAAA,UACvC,UAAU;AAAA,UACV,QAAQ;AAAA,UACR,SAAS;AAAA,QACV,CAAC;AAED,YAAIA,SAAQ,OAAOA,UAAS,YAAYA,MAAK,KAAK,GAAG;AACpD,gBAAM,YAAY,KAAK,qBAAqBA,KAAI;AAChD,cAAI,WAAW;AACd,mBAAO;AAAA,UACR;AAAA,QACD;AAAA,MACD,SAAS,OAAO;AAChB,kBAAU,EAAE,MAAM,oDAAoD,EAAE,MAAM,CAAC;AAAA,MAC/E;AAAA,IACD;AAGA,QAAI,OAAO;AAEX,QAAI,aAAa;AAChB,cAAQ,UAAU,WAAW;AAAA,IAC9B;AAEA,WAAO;AAAA,EACR;AAAA;AAAA;AAAA;AAAA;AAAA,EAMQ,kBAAkB,aAAuC;AAChE,UAAM,eAAe,cAClB;AAAA;AAAA,2CACsC,WAAW;AAAA,kBACpC,WAAW;AAAA,mBAExB;AAEH,WAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mEAM0D,cAAc;AAAA;AAAA,mBAAwB,WAAW,uBAAuB,EAAE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAY3I,YAAY;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQb;AAAA;AAAA;AAAA;AAAA;AAAA,EAMQ,qBAAqB,WAA2B;AACvD,QAAI,UAAU,UAAU,KAAK;AAG7B,UAAM,eAAe;AAAA,MACpB;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACD;AAEA,eAAW,WAAW,cAAc;AACnC,gBAAU,QAAQ,QAAQ,SAAS,EAAE,EAAE,KAAK;AAAA,IAC7C;AAGA,cAAU,QAAQ,QAAQ,iBAAiB,EAAE,EAAE,KAAK;AAGpD,QAAI,QAAQ,SAAS,GAAG,GAAG;AAC1B,YAAM,aAAa,QAAQ,QAAQ,GAAG;AACtC,YAAM,cAAc,QAAQ,UAAU,GAAG,UAAU,EAAE,KAAK,EAAE,YAAY;AAGxE,YAAM,iBAAiB;AAAA,QACtB;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACD;AAEA,YAAM,mBAAmB,eAAe,KAAK,eAAa,YAAY,SAAS,SAAS,CAAC;AAEzF,UAAI,kBAAkB;AACrB,cAAM,aAAa,QAAQ,UAAU,aAAa,CAAC,EAAE,KAAK;AAE1D,cAAM,iBAAiB,WAAW,QAAQ,iBAAiB,EAAE,EAAE,KAAK;AACpE,YAAI,kBAAkB,eAAe,SAAS,IAAI;AACjD,oBAAU;AAAA,QACX;AAAA,MACD;AAAA,IACD;AAGA,QAAK,QAAQ,WAAW,GAAG,KAAK,QAAQ,SAAS,GAAG,KAClD,QAAQ,WAAW,GAAG,KAAK,QAAQ,SAAS,GAAG,GAAI;AACpD,gBAAU,QAAQ,MAAM,GAAG,EAAE,EAAE,KAAK;AAAA,IACrC;AAEA,WAAO;AAAA,EACR;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWA,MAAM,SACL,YACA,OACA,MACA,YACA,KACkB;AAClB,QAAI;AAEH,YAAM,eAAe,MAAM,2BAA2B,KAAK,UAAU,GAAG;AAKxE,UAAI,YAAY;AAEhB,UAAI,iBAAiB,UAAU;AAG9B,cAAM,UAAU,MAAM,gBAAgB,GAAG;AACzC,cAAM,eAAe,QAAQ,KAAK,OAAK,EAAE,SAAS,QAAQ;AAE1D,YAAI,cAAc;AACjB,sBAAY,GAAG,aAAa,KAAK,IAAI,UAAU;AAChD,oBAAU,EAAE,MAAM,uCAAuC,SAAS,EAAE;AAAA,QACpE;AAAA,MACD;AAIA,YAAM,OAAO,CAAC,MAAM,UAAU,UAAU,WAAW,WAAW,OAAO,UAAU,MAAM,UAAU,UAAU;AAGzG,UAAI,iBAAiB,UAAU;AAC9B,cAAM,OAAO,MAAM,8BAA8B,KAAK,UAAU,GAAG;AACnE,aAAK,KAAK,UAAU,IAAI;AAAA,MACzB;AAGA,YAAM,SAAS,MAAM,iBAAyB,MAAM,MAAM,EAAE,IAAI,IAAI,MAAS;AAG7E,YAAM,MAAM,OAAO,WAAW,WAAW,OAAO,KAAK,IAAI,OAAO,MAAM,EAAE,KAAK;AAE7E,UAAI,CAAC,IAAI,SAAS,YAAY,KAAK,CAAC,IAAI,SAAS,QAAQ,GAAG;AAC3D,cAAM,IAAI,MAAM,0CAA0C,GAAG,EAAE;AAAA,MAChE;AAEA,aAAO;AAAA,IACR,SAAS,OAAO;AACf,YAAM,eAAe,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAG1E,UAAI,aAAa,SAAS,yBAAyB,KAAK,aAAa,SAAS,oBAAoB,GAAG;AACpG,cAAM,IAAI;AAAA,UACT,kCAAkC,YAAY;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,kCAKX,UAAU;AAAA;AAAA,QAE9C;AAAA,MACD;AAEA,YAAM,IAAI,MAAM,kCAAkC,YAAY,EAAE;AAAA,IACjE;AAAA,EACD;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,gBAAgB,KAA4B;AACjD,QAAI;AACH,YAAM,YAAY,GAAG;AACtB,gBAAU,EAAE,MAAM,wBAAwB,EAAE,IAAI,CAAC;AAAA,IACjD,SAAS,OAAO;AAEhB,gBAAU,EAAE,KAAK,gCAAgC,EAAE,MAAM,CAAC;AAAA,IAC1D;AAAA,EACD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYA,MAAM,eACL,YACA,OACA,aACA,YACA,cACA,eAC4B;AAE5B,UAAM,aAAa,MAAM,KAAK,mBAAmB,YAAY,YAAY;AAEzE,QAAI,YAAY;AAChB,gBAAU,EAAE,KAAK,gCAAgC,WAAW,GAAG,EAAE;AAEhE,UAAI,eAAe;AAClB,cAAM,KAAK,gBAAgB,WAAW,GAAG;AAAA,MAC1C;AAEA,aAAO;AAAA,QACN,KAAK,WAAW;AAAA,QAChB,QAAQ,WAAW;AAAA,QACnB,aAAa;AAAA,MACd;AAAA,IACD;AAGA,UAAM,OAAO,MAAM,KAAK,eAAe,aAAa,YAAY;AAGjE,cAAU,EAAE,KAAK,0BAA0B;AAC1C,UAAM,MAAM,MAAM,KAAK,SAAS,YAAY,OAAO,MAAM,YAAY,YAAY;AAGjF,UAAM,WAAW,KAAK,uBAAuB,GAAG;AAEhD,QAAI,eAAe;AAClB,YAAM,KAAK,gBAAgB,GAAG;AAAA,IAC/B;AAEA,WAAO;AAAA,MACN;AAAA,MACA,QAAQ;AAAA,MACR,aAAa;AAAA,IACd;AAAA,EACD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOQ,uBAAuB,KAAqB;AACnD,UAAM,QAAQ,IAAI,MAAM,eAAe;AACvC,QAAI,+BAAQ,IAAI;AACf,aAAO,SAAS,MAAM,CAAC,GAAG,EAAE;AAAA,IAC7B;AACA,UAAM,IAAI,MAAM,yCAAyC,GAAG,EAAE;AAAA,EAC/D;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYA,MAAM,cACL,YACA,OACA,MACA,YACA,KAC2C;AAC3C,QAAI;AAEH,YAAM,eAAe,MAAM,2BAA2B,KAAK,UAAU,GAAG;AAIxE,UAAI,YAAY;AAEhB,UAAI,iBAAiB,UAAU;AAE9B,cAAM,UAAU,MAAM,gBAAgB,GAAG;AACzC,cAAM,eAAe,QAAQ,KAAK,OAAK,EAAE,SAAS,QAAQ;AAE1D,YAAI,cAAc;AACjB,sBAAY,GAAG,aAAa,KAAK,IAAI,UAAU;AAC/C,oBAAU,EAAE,MAAM,uCAAuC,SAAS,EAAE;AAAA,QACrE;AAAA,MACD;AAGA,YAAM,OAAO,CAAC,MAAM,UAAU,UAAU,WAAW,WAAW,OAAO,UAAU,MAAM,UAAU,YAAY,SAAS;AAGpH,UAAI,iBAAiB,UAAU;AAC9B,cAAM,OAAO,MAAM,8BAA8B,KAAK,UAAU,GAAG;AACnE,aAAK,KAAK,UAAU,IAAI;AAAA,MACzB;AAGA,YAAM,SAAS,MAAM,iBAAyB,MAAM,MAAM,EAAE,IAAI,IAAI,MAAS;AAG7E,YAAM,MAAM,OAAO,WAAW,WAAW,OAAO,KAAK,IAAI,OAAO,MAAM,EAAE,KAAK;AAE7E,UAAI,CAAC,IAAI,SAAS,YAAY,KAAK,CAAC,IAAI,SAAS,QAAQ,GAAG;AAC3D,cAAM,IAAI,MAAM,kDAAkD,GAAG,EAAE;AAAA,MACxE;AAEA,YAAM,SAAS,KAAK,uBAAuB,GAAG;AAE9C,aAAO,EAAE,KAAK,OAAO;AAAA,IACtB,SAAS,OAAO;AACf,YAAM,eAAe,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAG1E,UAAI,aAAa,SAAS,yBAAyB,KAAK,aAAa,SAAS,oBAAoB,GAAG;AACpG,cAAM,IAAI;AAAA,UACT,wCAAwC,YAAY;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,kCAIjB,UAAU;AAAA;AAAA,QAE9C;AAAA,MACD;AAEA,YAAM,IAAI,MAAM,wCAAwC,YAAY,EAAE;AAAA,IACvE;AAAA,EACD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,YAAY,UAAkB,KAA6B;AAChE,UAAM,OAAO,CAAC,MAAM,SAAS,OAAO,QAAQ,CAAC;AAC7C,UAAM,iBAAiB,MAAM,MAAM,EAAE,IAAI,IAAI,MAAS;AACtD,cAAU,EAAE,KAAK,cAAc,QAAQ,sBAAsB;AAAA,EAC9D;AACD;","names":["body"]}
|
|
@@ -403,11 +403,16 @@ var FirstRunManager = class {
|
|
|
403
403
|
|
|
404
404
|
// src/utils/mcp.ts
|
|
405
405
|
import path2 from "path";
|
|
406
|
-
|
|
406
|
+
import os2 from "os";
|
|
407
|
+
async function generateIssueManagementMcpConfig(contextType, repo, provider = "github", settings, draftPrNumber) {
|
|
407
408
|
var _a, _b;
|
|
409
|
+
const effectiveContextType = draftPrNumber ? "pr" : contextType;
|
|
408
410
|
let envVars = {
|
|
409
411
|
ISSUE_PROVIDER: provider
|
|
410
412
|
};
|
|
413
|
+
if (draftPrNumber) {
|
|
414
|
+
envVars.DRAFT_PR_NUMBER = String(draftPrNumber);
|
|
415
|
+
}
|
|
411
416
|
if (provider === "github") {
|
|
412
417
|
let owner;
|
|
413
418
|
let name;
|
|
@@ -423,7 +428,7 @@ async function generateIssueManagementMcpConfig(contextType, repo, provider = "g
|
|
|
423
428
|
owner = repoInfo.owner;
|
|
424
429
|
name = repoInfo.name;
|
|
425
430
|
}
|
|
426
|
-
const githubEventName =
|
|
431
|
+
const githubEventName = effectiveContextType === "issue" ? "issues" : effectiveContextType === "pr" ? "pull_request" : void 0;
|
|
427
432
|
envVars = {
|
|
428
433
|
...envVars,
|
|
429
434
|
REPO_OWNER: owner,
|
|
@@ -435,8 +440,9 @@ async function generateIssueManagementMcpConfig(contextType, repo, provider = "g
|
|
|
435
440
|
provider,
|
|
436
441
|
repoOwner: owner,
|
|
437
442
|
repoName: name,
|
|
438
|
-
contextType:
|
|
439
|
-
githubEventName: githubEventName ?? "auto-detect"
|
|
443
|
+
contextType: effectiveContextType ?? "auto-detect",
|
|
444
|
+
githubEventName: githubEventName ?? "auto-detect",
|
|
445
|
+
draftPrNumber: draftPrNumber ?? void 0
|
|
440
446
|
});
|
|
441
447
|
} else {
|
|
442
448
|
const apiToken = ((_b = (_a = settings == null ? void 0 : settings.issueManagement) == null ? void 0 : _a.linear) == null ? void 0 : _b.apiToken) ?? process.env.LINEAR_API_TOKEN;
|
|
@@ -461,10 +467,47 @@ async function generateIssueManagementMcpConfig(contextType, repo, provider = "g
|
|
|
461
467
|
};
|
|
462
468
|
return [mcpServerConfig];
|
|
463
469
|
}
|
|
470
|
+
function slugifyPath(loomPath) {
|
|
471
|
+
let slug = loomPath.replace(/[/\\]+$/, "");
|
|
472
|
+
slug = slug.replace(/[/\\]/g, "___");
|
|
473
|
+
slug = slug.replace(/[^a-zA-Z0-9_-]/g, "-");
|
|
474
|
+
return `${slug}.json`;
|
|
475
|
+
}
|
|
476
|
+
function generateRecapMcpConfig(loomPath, loomMetadata) {
|
|
477
|
+
const recapsDir = path2.join(os2.homedir(), ".config", "iloom-ai", "recaps");
|
|
478
|
+
const recapFilePath = path2.join(recapsDir, slugifyPath(loomPath));
|
|
479
|
+
const envVars = {
|
|
480
|
+
RECAP_FILE_PATH: recapFilePath,
|
|
481
|
+
LOOM_METADATA_JSON: JSON.stringify(loomMetadata)
|
|
482
|
+
};
|
|
483
|
+
logger.debug("Generated MCP config for recap server", {
|
|
484
|
+
loomPath,
|
|
485
|
+
recapFilePath,
|
|
486
|
+
loomMetadataDescription: loomMetadata.description
|
|
487
|
+
});
|
|
488
|
+
return [
|
|
489
|
+
{
|
|
490
|
+
mcpServers: {
|
|
491
|
+
recap: {
|
|
492
|
+
transport: "stdio",
|
|
493
|
+
command: "node",
|
|
494
|
+
args: [
|
|
495
|
+
path2.join(
|
|
496
|
+
path2.dirname(new globalThis.URL(import.meta.url).pathname),
|
|
497
|
+
"../dist/mcp/recap-server.js"
|
|
498
|
+
)
|
|
499
|
+
],
|
|
500
|
+
env: envVars
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
];
|
|
505
|
+
}
|
|
464
506
|
|
|
465
507
|
export {
|
|
466
508
|
IssueTrackerFactory,
|
|
467
509
|
FirstRunManager,
|
|
468
|
-
generateIssueManagementMcpConfig
|
|
510
|
+
generateIssueManagementMcpConfig,
|
|
511
|
+
generateRecapMcpConfig
|
|
469
512
|
};
|
|
470
|
-
//# sourceMappingURL=chunk-
|
|
513
|
+
//# sourceMappingURL=chunk-RFUOIUQF.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/lib/LinearService.ts","../src/lib/IssueTrackerFactory.ts","../src/utils/FirstRunManager.ts","../src/utils/mcp.ts"],"sourcesContent":["/**\n * LinearService - IssueTracker implementation for Linear\n * Implements issue tracking operations using the @linear/sdk\n */\n\nimport type { Issue, PullRequest, IssueTrackerInputDetection } from '../types/index.js'\nimport type { LinearIssue } from '../types/linear.js'\nimport { LinearServiceError } from '../types/linear.js'\nimport {\n fetchLinearIssue,\n createLinearIssue,\n updateLinearIssueState,\n} from '../utils/linear.js'\nimport { promptConfirmation } from '../utils/prompt.js'\nimport type { IssueTracker } from './IssueTracker.js'\nimport { logger } from '../utils/logger.js'\n\n/**\n * Linear service configuration options\n */\nexport interface LinearServiceConfig {\n /** Linear team key (e.g., \"ENG\", \"PLAT\") */\n teamId?: string\n /** Branch naming template (e.g., \"feat/{{key}}__{{title}}\") */\n branchFormat?: string\n}\n\n/**\n * Linear implementation of IssueTracker interface\n */\nexport class LinearService implements IssueTracker {\n // IssueTracker interface implementation\n readonly providerName = 'linear'\n readonly supportsPullRequests = false // Linear doesn't have pull requests\n\n private config: LinearServiceConfig\n private prompter: (message: string) => Promise<boolean>\n\n constructor(\n config?: LinearServiceConfig,\n options?: { prompter?: (message: string) => Promise<boolean> },\n ) {\n this.config = config ?? {}\n this.prompter = options?.prompter ?? promptConfirmation\n }\n\n /**\n * Detect if input matches Linear identifier format (TEAM-NUMBER)\n * @param input - User input string\n * @param _repo - Repository (unused for Linear)\n * @returns Detection result with type and identifier\n */\n public async detectInputType(\n input: string,\n _repo?: string,\n ): Promise<IssueTrackerInputDetection> {\n logger.debug(`LinearService.detectInputType called with input: \"${input}\"`)\n\n // Pattern: TEAM-NUMBER (e.g., ENG-123, PLAT-456)\n // Requires at least 2 letters before dash to avoid conflict with PR-123 format\n const linearPattern = /^([A-Z]{2,}-\\d+)$/i\n const match = input.match(linearPattern)\n\n if (!match?.[1]) {\n logger.debug(`LinearService: Input \"${input}\" does not match Linear pattern`)\n return { type: 'unknown', identifier: null, rawInput: input }\n }\n\n const identifier = match[1].toUpperCase()\n logger.debug(`LinearService: Matched Linear identifier: ${identifier}`)\n\n // Validate the issue exists in Linear\n logger.debug(`LinearService: Checking if ${identifier} is a valid Linear issue via SDK`)\n const issue = await this.isValidIssue(identifier)\n\n if (issue) {\n logger.debug(`LinearService: Issue ${identifier} found: \"${issue.title}\"`)\n return { type: 'issue', identifier, rawInput: input }\n }\n\n // Not found\n logger.debug(`LinearService: Issue ${identifier} NOT found by SDK`)\n return { type: 'unknown', identifier: null, rawInput: input }\n }\n\n /**\n * Fetch a Linear issue by identifier\n * @param identifier - Linear issue identifier (string or number)\n * @param _repo - Repository (unused for Linear)\n * @returns Generic Issue type\n * @throws LinearServiceError if issue not found\n */\n public async fetchIssue(identifier: string | number, _repo?: string): Promise<Issue> {\n const linearIssue = await fetchLinearIssue(String(identifier))\n return this.mapLinearIssueToIssue(linearIssue)\n }\n\n /**\n * Check if an issue identifier is valid (silent validation)\n * @param identifier - Linear issue identifier\n * @param _repo - Repository (unused for Linear)\n * @returns Issue if valid, false if not found\n */\n public async isValidIssue(identifier: string | number, _repo?: string): Promise<Issue | false> {\n try {\n return await this.fetchIssue(identifier)\n } catch (error) {\n // Return false for NOT_FOUND errors (expected during detection)\n if (error instanceof LinearServiceError && error.code === 'NOT_FOUND') {\n return false\n }\n // Re-throw unexpected errors\n throw error\n }\n }\n\n /**\n * Validate issue state and prompt user if closed\n * @param issue - Issue to validate\n * @throws LinearServiceError if user cancels due to closed issue\n */\n public async validateIssueState(issue: Issue): Promise<void> {\n if (issue.state === 'closed') {\n const shouldContinue = await this.prompter(\n `Issue ${issue.number} is closed. Continue anyway?`,\n )\n\n if (!shouldContinue) {\n throw new LinearServiceError('INVALID_STATE', 'User cancelled due to closed issue')\n }\n }\n }\n\n /**\n * Create a new Linear issue\n * @param title - Issue title\n * @param body - Issue description (markdown)\n * @param _repository - Repository (unused for Linear)\n * @param labels - Optional label names\n * @returns Created issue identifier and URL\n * @throws LinearServiceError if teamId not configured or creation fails\n */\n public async createIssue(\n title: string,\n body: string,\n _repository?: string,\n labels?: string[],\n ): Promise<{ number: string | number; url: string }> {\n // Require teamId configuration\n if (!this.config.teamId) {\n throw new LinearServiceError(\n 'INVALID_STATE',\n 'Linear teamId not configured. Run `il init` to configure Linear settings.',\n )\n }\n\n logger.info(`Creating Linear issue in team ${this.config.teamId}: ${title}`)\n\n const result = await createLinearIssue(title, body, this.config.teamId, labels)\n\n return {\n number: result.identifier,\n url: result.url,\n }\n }\n\n /**\n * Get the web URL for a Linear issue\n * @param identifier - Linear issue identifier\n * @param _repo - Repository (unused for Linear)\n * @returns Issue URL\n */\n public async getIssueUrl(identifier: string | number, _repo?: string): Promise<string> {\n const issue = await this.fetchIssue(identifier)\n return issue.url\n }\n\n /**\n * Move a Linear issue to \"In Progress\" state\n * @param identifier - Linear issue identifier\n * @throws LinearServiceError if state update fails\n */\n public async moveIssueToInProgress(identifier: string | number): Promise<void> {\n logger.info(`Moving Linear issue ${identifier} to In Progress`)\n await updateLinearIssueState(String(identifier), 'In Progress')\n }\n\n /**\n * Extract issue context for AI prompts\n * @param entity - Issue (Linear doesn't have PRs)\n * @returns Formatted context string\n */\n public extractContext(entity: Issue | PullRequest): string {\n // Linear doesn't have PRs, always an issue\n const issue = entity as Issue\n return `Linear Issue ${issue.number}: ${issue.title}\\nState: ${issue.state}\\n\\n${issue.body}`\n }\n\n /**\n * Map Linear API issue to generic Issue type\n * @param linear - Linear issue from SDK\n * @returns Generic Issue type\n */\n private mapLinearIssueToIssue(linear: LinearIssue): Issue {\n return {\n number: linear.identifier, // Keep as string (e.g., \"ENG-123\")\n title: linear.title,\n body: linear.description ?? '',\n state: linear.state ? (linear.state.toLowerCase().includes('done') || linear.state.toLowerCase().includes('completed') || linear.state.toLowerCase().includes('canceled') ? 'closed' : 'open') : 'open',\n labels: [],\n assignees: [],\n url: linear.url,\n }\n }\n}\n","// IssueTrackerFactory - creates appropriate IssueTracker based on settings\n// Follows pattern from database provider instantiation\n\nimport type { IssueTracker } from './IssueTracker.js'\nimport { GitHubService } from './GitHubService.js'\nimport { LinearService, type LinearServiceConfig } from './LinearService.js'\nimport type { IloomSettings } from './SettingsManager.js'\nimport { getLogger } from '../utils/logger-context.js'\n\nexport type IssueTrackerProviderType = 'github' | 'linear'\n\n/**\n * Factory for creating IssueTracker instances based on settings\n * Provides a single point of provider instantiation\n *\n * Usage:\n * const tracker = IssueTrackerFactory.create(settings, { useClaude: true })\n * const issue = await tracker.fetchIssue(123)\n */\nexport class IssueTrackerFactory {\n\t/**\n\t * Create an IssueTracker instance based on settings configuration\n\t * Defaults to GitHub if no provider specified\n\t *\n\t * @param settings - iloom settings containing issueManagement.provider\n\t * @returns IssueTracker instance configured for the specified provider\n\t * @throws Error if provider type is not supported\n\t */\n\tstatic create(settings: IloomSettings): IssueTracker {\n\t\tconst provider = settings.issueManagement?.provider ?? 'github'\n\n\t\tgetLogger().debug(`IssueTrackerFactory: Creating tracker for provider \"${provider}\"`)\n\t\tgetLogger().debug(`IssueTrackerFactory: issueManagement settings:`, JSON.stringify(settings.issueManagement, null, 2))\n\n\t\tswitch (provider) {\n\t\t\tcase 'github':\n\t\t\t\tgetLogger().debug('IssueTrackerFactory: Creating GitHubService')\n\t\t\t\treturn new GitHubService()\n\t\t\tcase 'linear': {\n\t\t\t\tconst linearSettings = settings.issueManagement?.linear\n\t\t\t\tconst linearConfig: LinearServiceConfig = {}\n\n\t\t\t\tif (linearSettings?.teamId) {\n\t\t\t\t\tlinearConfig.teamId = linearSettings.teamId\n\t\t\t\t}\n\t\t\t\tif (linearSettings?.branchFormat) {\n\t\t\t\t\tlinearConfig.branchFormat = linearSettings.branchFormat\n\t\t\t\t}\n\n\t\t\t\tgetLogger().debug(`IssueTrackerFactory: Creating LinearService with config:`, JSON.stringify(linearConfig, null, 2))\n\t\t\t\treturn new LinearService(linearConfig)\n\t\t\t}\n\t\t\tdefault:\n\t\t\t\tthrow new Error(`Unsupported issue tracker provider: ${provider}`)\n\t\t}\n\t}\n\n\t/**\n\t * Get the configured provider name from settings\n\t * Defaults to 'github' if not configured\n\t *\n\t * @param settings - iloom settings\n\t * @returns Provider type string\n\t */\n\tstatic getProviderName(settings: IloomSettings): IssueTrackerProviderType {\n\t\treturn (settings.issueManagement?.provider ?? 'github') as IssueTrackerProviderType\n\t}\n}\n","import os from 'os'\nimport path from 'path'\nimport fs from 'fs-extra'\nimport { logger } from './logger.js'\n\n/**\n * FirstRunManager: Detect and track first-time spin usage via marker file\n *\n * Follows the same pattern as UpdateNotifier for file-based state tracking\n * in ~/.config/iloom-ai/ directory.\n *\n * Also supports project-level tracking for config wizard completion using\n * individual marker files per project in ~/.config/iloom-ai/projects/\n */\nexport class FirstRunManager {\n\tprivate markerFilePath: string\n\tprivate configDir: string\n\n\tconstructor(feature: string = 'spin') {\n\t\tthis.configDir = path.join(os.homedir(), '.config', 'iloom-ai')\n\t\tthis.markerFilePath = path.join(this.configDir, `${feature}-first-run`)\n\t\tlogger.debug('FirstRunManager initialized', {\n\t\t\tfeature,\n\t\t\tmarkerFilePath: this.markerFilePath\n\t\t})\n\t}\n\n\t/**\n\t * Get the directory for project marker files\n\t */\n\tprivate getProjectsDir(): string {\n\t\treturn path.join(this.configDir, 'projects')\n\t}\n\n\t/**\n\t * Resolve symlinks in project path to get canonical path\n\t * Falls back to original path on errors (broken symlinks, permissions, etc.)\n\t */\n\tprivate async resolveProjectPath(projectPath: string): Promise<string> {\n\t\ttry {\n\t\t\treturn await fs.realpath(projectPath)\n\t\t} catch {\n\t\t\tlogger.debug('resolveProjectPath: Failed to resolve symlink, using original path', { projectPath })\n\t\t\treturn projectPath\n\t\t}\n\t}\n\n\t/**\n\t * Convert a project path to a readable filename\n\t * /Users/adam/Projects/my-app -> Users__adam__Projects__my-app\n\t */\n\tprivate projectPathToFileName(projectPath: string): string {\n\t\tconst normalized = path.normalize(projectPath)\n\t\treturn normalized.replace(/^[/\\\\]+/, '').replace(/[/\\\\]+/g, '__')\n\t}\n\n\t/**\n\t * Get full path to a project's marker file\n\t */\n\tprivate getProjectMarkerPath(projectPath: string): string {\n\t\treturn path.join(this.getProjectsDir(), this.projectPathToFileName(projectPath))\n\t}\n\n\t/**\n\t * Extract project name from path\n\t */\n\tprivate getProjectName(projectPath: string): string {\n\t\treturn path.basename(projectPath)\n\t}\n\n\t/**\n\t * Check if a project has been configured\n\t * Returns true if project marker file exists\n\t */\n\tasync isProjectConfigured(projectPath?: string): Promise<boolean> {\n\t\tconst inputPath = projectPath ?? process.cwd()\n\t\tconst resolvedPath = await this.resolveProjectPath(inputPath)\n\t\tconst markerPath = this.getProjectMarkerPath(resolvedPath)\n\t\tlogger.debug('isProjectConfigured: Checking for marker file', { markerPath })\n\t\ttry {\n\t\t\tconst exists = await fs.pathExists(markerPath)\n\t\t\tlogger.debug(`isProjectConfigured: Marker file exists=${exists}`)\n\t\t\treturn exists\n\t\t} catch (error) {\n\t\t\t// On error, treat as not configured to allow wizard to run\n\t\t\tlogger.debug(`isProjectConfigured: Error checking marker file, treating as not configured: ${error}`)\n\t\t\treturn false\n\t\t}\n\t}\n\n\t/**\n\t * Check if a project has local .iloom settings files\n\t * Returns true if .iloom/settings.json or .iloom/settings.local.json exists with content\n\t */\n\tprivate async hasLocalSettings(projectPath: string): Promise<boolean> {\n\t\tconst iloomDir = path.join(projectPath, '.iloom')\n\t\tconst settingsPath = path.join(iloomDir, 'settings.json')\n\t\tconst settingsLocalPath = path.join(iloomDir, 'settings.local.json')\n\n\t\tconst hasSettings = await this.hasNonEmptySettingsFile(settingsPath)\n\t\tconst hasLocalSettings = await this.hasNonEmptySettingsFile(settingsLocalPath)\n\n\t\treturn hasSettings || hasLocalSettings\n\t}\n\n\t/**\n\t * Check if a settings file exists and has non-empty content\n\t */\n\tprivate async hasNonEmptySettingsFile(filePath: string): Promise<boolean> {\n\t\ttry {\n\t\t\tconst exists = await fs.pathExists(filePath)\n\t\t\tif (!exists) return false\n\t\t\tconst content = await fs.readFile(filePath, 'utf8')\n\t\t\tconst parsed = JSON.parse(content)\n\t\t\treturn Object.keys(parsed).length > 0\n\t\t} catch {\n\t\t\treturn false\n\t\t}\n\t}\n\n\t/**\n\t * Fixup legacy projects that have local config but lack the global marker file\n\t * This handles projects configured before global project tracking was implemented\n\t *\n\t * Returns object with:\n\t * - isConfigured: true if project is configured (either already had marker or fixup created one)\n\t * - wasFixedUp: true if fixup was performed (marker was created by this call)\n\t *\n\t * This combined return avoids duplicate isProjectConfigured() calls in needsFirstRunSetup()\n\t */\n\tasync fixupLegacyProject(projectPath?: string): Promise<{ isConfigured: boolean; wasFixedUp: boolean }> {\n\t\tconst inputPath = projectPath ?? process.cwd()\n\t\tconst resolvedPath = await this.resolveProjectPath(inputPath)\n\t\tlogger.debug('fixupLegacyProject: Checking for legacy project', { projectPath: resolvedPath })\n\n\t\t// Check if already has global marker - no fixup needed, but project IS configured\n\t\tconst hasMarker = await this.isProjectConfigured(resolvedPath)\n\t\tif (hasMarker) {\n\t\t\tlogger.debug('fixupLegacyProject: Project already has global marker')\n\t\t\treturn { isConfigured: true, wasFixedUp: false }\n\t\t}\n\n\t\t// Check if has local settings files - this indicates a legacy configured project\n\t\tconst hasLocal = await this.hasLocalSettings(resolvedPath)\n\t\tif (!hasLocal) {\n\t\t\tlogger.debug('fixupLegacyProject: No local settings found, not a legacy project')\n\t\t\treturn { isConfigured: false, wasFixedUp: false }\n\t\t}\n\n\t\t// Legacy project found - create the missing global marker\n\t\tlogger.debug('fixupLegacyProject: Legacy project detected, creating global marker')\n\t\tawait this.markProjectAsConfigured(resolvedPath)\n\t\treturn { isConfigured: true, wasFixedUp: true }\n\t}\n\n\t/**\n\t * Mark a project as configured\n\t * Creates a marker file with project metadata\n\t */\n\tasync markProjectAsConfigured(projectPath?: string): Promise<void> {\n\t\tconst inputPath = projectPath ?? process.cwd()\n\t\tconst resolvedPath = await this.resolveProjectPath(inputPath)\n\t\tconst markerPath = this.getProjectMarkerPath(resolvedPath)\n\t\tlogger.debug('markProjectAsConfigured: Creating marker file', { markerPath })\n\t\ttry {\n\t\t\tawait fs.ensureDir(this.getProjectsDir())\n\t\t\tconst markerContent = {\n\t\t\t\tconfiguredAt: new Date().toISOString(),\n\t\t\t\tprojectPath: resolvedPath,\n\t\t\t\tprojectName: this.getProjectName(resolvedPath)\n\t\t\t}\n\t\t\tawait fs.writeFile(markerPath, JSON.stringify(markerContent, null, 2), 'utf8')\n\t\t\tlogger.debug('markProjectAsConfigured: Marker file created successfully')\n\t\t} catch (error) {\n\t\t\t// Don't throw on errors - just log debug message\n\t\t\tlogger.debug(`markProjectAsConfigured: Failed to create marker file: ${error}`)\n\t\t}\n\t}\n\n\t/**\n\t * Check if this is the first run of the feature\n\t * Returns true if marker file doesn't exist\n\t * Handles errors gracefully by returning true (treat as first-run on error)\n\t */\n\tasync isFirstRun(): Promise<boolean> {\n\t\tlogger.debug('isFirstRun: Checking for marker file', { markerFilePath: this.markerFilePath })\n\t\ttry {\n\t\t\tconst exists = await fs.pathExists(this.markerFilePath)\n\t\t\tlogger.debug(`isFirstRun: Marker file exists=${exists}`)\n\t\t\treturn !exists\n\t\t} catch (error) {\n\t\t\t// On error, gracefully degrade by treating as first-run\n\t\t\tlogger.debug(`isFirstRun: Error checking marker file, treating as first-run: ${error}`)\n\t\t\treturn true\n\t\t}\n\t}\n\n\t/**\n\t * Mark the feature as having been run\n\t * Creates the marker file in config directory\n\t * Handles errors gracefully without throwing\n\t */\n\tasync markAsRun(): Promise<void> {\n\t\tlogger.debug('markAsRun: Attempting to create marker file', { markerFilePath: this.markerFilePath })\n\t\ttry {\n\t\t\t// Ensure directory exists\n\t\t\tconst configDir = path.dirname(this.markerFilePath)\n\t\t\tlogger.debug(`markAsRun: Ensuring config directory exists: ${configDir}`)\n\t\t\tawait fs.ensureDir(configDir)\n\n\t\t\t// Write marker file with timestamp for debugging\n\t\t\tconst markerContent = {\n\t\t\t\tfirstRun: new Date().toISOString(),\n\t\t\t}\n\t\t\tawait fs.writeFile(this.markerFilePath, JSON.stringify(markerContent, null, 2), 'utf8')\n\t\t\tlogger.debug('markAsRun: Marker file created successfully')\n\t\t} catch (error) {\n\t\t\t// Don't throw on errors - just log debug message\n\t\t\t// Failing to write marker shouldn't break the workflow\n\t\t\tlogger.debug(`markAsRun: Failed to create marker file: ${error}`)\n\t\t}\n\t}\n}\n","import path from 'path'\nimport { getRepoInfo } from './github.js'\nimport { logger } from './logger.js'\nimport type { IloomSettings } from '../lib/SettingsManager.js'\n\n/**\n * Generate MCP configuration for issue management\n * Uses a single server that can handle both issues and pull requests\n * Returns array of MCP server config objects\n * @param contextType - Optional context type (issue or pr)\n * @param repo - Optional repo in \"owner/repo\" format. If not provided, will auto-detect from git.\n * @param provider - Issue management provider (default: 'github')\n * @param settings - Optional settings to extract Linear API token from\n */\nexport async function generateIssueManagementMcpConfig(\n\tcontextType?: 'issue' | 'pr',\n\trepo?: string,\n\tprovider: 'github' | 'linear' = 'github',\n\tsettings?: IloomSettings\n): Promise<Record<string, unknown>[]> {\n\t// Build provider-specific environment variables\n\tlet envVars: Record<string, string> = {\n\t\tISSUE_PROVIDER: provider,\n\t}\n\n\tif (provider === 'github') {\n\t\t// Get repository information for GitHub - either from provided repo string or auto-detect\n\t\tlet owner: string\n\t\tlet name: string\n\n\t\tif (repo) {\n\t\t\tconst parts = repo.split('/')\n\t\t\tif (parts.length !== 2 || !parts[0] || !parts[1]) {\n\t\t\t\tthrow new Error(`Invalid repo format: ${repo}. Expected \"owner/repo\"`)\n\t\t\t}\n\t\t\towner = parts[0]\n\t\t\tname = parts[1]\n\t\t} else {\n\t\t\tconst repoInfo = await getRepoInfo()\n\t\t\towner = repoInfo.owner\n\t\t\tname = repoInfo.name\n\t\t}\n\n\t\t// Map logical types to GitHub's webhook event names (handle GitHub's naming quirk here)\n\t\tconst githubEventName = contextType === 'issue' ? 'issues' : contextType === 'pr' ? 'pull_request' : undefined\n\n\t\tenvVars = {\n\t\t\t...envVars,\n\t\t\tREPO_OWNER: owner,\n\t\t\tREPO_NAME: name,\n\t\t\tGITHUB_API_URL: 'https://api.github.com/',\n\t\t\t...(githubEventName && { GITHUB_EVENT_NAME: githubEventName }),\n\t\t}\n\n\t\tlogger.debug('Generated MCP config for GitHub issue management', {\n\t\t\tprovider,\n\t\t\trepoOwner: owner,\n\t\t\trepoName: name,\n\t\t\tcontextType: contextType ?? 'auto-detect',\n\t\t\tgithubEventName: githubEventName ?? 'auto-detect'\n\t\t})\n\t} else {\n\t\t// Linear needs API token passed through\n\t\tconst apiToken = settings?.issueManagement?.linear?.apiToken ?? process.env.LINEAR_API_TOKEN\n\n\t\tif (apiToken) {\n\t\t\tenvVars.LINEAR_API_TOKEN = apiToken\n\t\t}\n\n\t\tlogger.debug('Generated MCP config for Linear issue management', {\n\t\t\tprovider,\n\t\t\thasApiToken: !!apiToken,\n\t\t\tcontextType: contextType ?? 'auto-detect',\n\t\t})\n\t}\n\n\t// Generate single MCP server config\n\tconst mcpServerConfig = {\n\t\tmcpServers: {\n\t\t\tissue_management: {\n\t\t\t\ttransport: 'stdio',\n\t\t\t\tcommand: 'node',\n\t\t\t\targs: [path.join(path.dirname(new globalThis.URL(import.meta.url).pathname), '../dist/mcp/issue-management-server.js')],\n\t\t\t\tenv: envVars,\n\t\t\t},\n\t\t},\n\t}\n\n\treturn [mcpServerConfig]\n}"],"mappings":"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","names":["path","path"]}
|
|
1
|
+
{"version":3,"sources":["../src/lib/LinearService.ts","../src/lib/IssueTrackerFactory.ts","../src/utils/FirstRunManager.ts","../src/utils/mcp.ts"],"sourcesContent":["/**\n * LinearService - IssueTracker implementation for Linear\n * Implements issue tracking operations using the @linear/sdk\n */\n\nimport type { Issue, PullRequest, IssueTrackerInputDetection } from '../types/index.js'\nimport type { LinearIssue } from '../types/linear.js'\nimport { LinearServiceError } from '../types/linear.js'\nimport {\n fetchLinearIssue,\n createLinearIssue,\n updateLinearIssueState,\n} from '../utils/linear.js'\nimport { promptConfirmation } from '../utils/prompt.js'\nimport type { IssueTracker } from './IssueTracker.js'\nimport { logger } from '../utils/logger.js'\n\n/**\n * Linear service configuration options\n */\nexport interface LinearServiceConfig {\n /** Linear team key (e.g., \"ENG\", \"PLAT\") */\n teamId?: string\n /** Branch naming template (e.g., \"feat/{{key}}__{{title}}\") */\n branchFormat?: string\n}\n\n/**\n * Linear implementation of IssueTracker interface\n */\nexport class LinearService implements IssueTracker {\n // IssueTracker interface implementation\n readonly providerName = 'linear'\n readonly supportsPullRequests = false // Linear doesn't have pull requests\n\n private config: LinearServiceConfig\n private prompter: (message: string) => Promise<boolean>\n\n constructor(\n config?: LinearServiceConfig,\n options?: { prompter?: (message: string) => Promise<boolean> },\n ) {\n this.config = config ?? {}\n this.prompter = options?.prompter ?? promptConfirmation\n }\n\n /**\n * Detect if input matches Linear identifier format (TEAM-NUMBER)\n * @param input - User input string\n * @param _repo - Repository (unused for Linear)\n * @returns Detection result with type and identifier\n */\n public async detectInputType(\n input: string,\n _repo?: string,\n ): Promise<IssueTrackerInputDetection> {\n logger.debug(`LinearService.detectInputType called with input: \"${input}\"`)\n\n // Pattern: TEAM-NUMBER (e.g., ENG-123, PLAT-456)\n // Requires at least 2 letters before dash to avoid conflict with PR-123 format\n const linearPattern = /^([A-Z]{2,}-\\d+)$/i\n const match = input.match(linearPattern)\n\n if (!match?.[1]) {\n logger.debug(`LinearService: Input \"${input}\" does not match Linear pattern`)\n return { type: 'unknown', identifier: null, rawInput: input }\n }\n\n const identifier = match[1].toUpperCase()\n logger.debug(`LinearService: Matched Linear identifier: ${identifier}`)\n\n // Validate the issue exists in Linear\n logger.debug(`LinearService: Checking if ${identifier} is a valid Linear issue via SDK`)\n const issue = await this.isValidIssue(identifier)\n\n if (issue) {\n logger.debug(`LinearService: Issue ${identifier} found: \"${issue.title}\"`)\n return { type: 'issue', identifier, rawInput: input }\n }\n\n // Not found\n logger.debug(`LinearService: Issue ${identifier} NOT found by SDK`)\n return { type: 'unknown', identifier: null, rawInput: input }\n }\n\n /**\n * Fetch a Linear issue by identifier\n * @param identifier - Linear issue identifier (string or number)\n * @param _repo - Repository (unused for Linear)\n * @returns Generic Issue type\n * @throws LinearServiceError if issue not found\n */\n public async fetchIssue(identifier: string | number, _repo?: string): Promise<Issue> {\n const linearIssue = await fetchLinearIssue(String(identifier))\n return this.mapLinearIssueToIssue(linearIssue)\n }\n\n /**\n * Check if an issue identifier is valid (silent validation)\n * @param identifier - Linear issue identifier\n * @param _repo - Repository (unused for Linear)\n * @returns Issue if valid, false if not found\n */\n public async isValidIssue(identifier: string | number, _repo?: string): Promise<Issue | false> {\n try {\n return await this.fetchIssue(identifier)\n } catch (error) {\n // Return false for NOT_FOUND errors (expected during detection)\n if (error instanceof LinearServiceError && error.code === 'NOT_FOUND') {\n return false\n }\n // Re-throw unexpected errors\n throw error\n }\n }\n\n /**\n * Validate issue state and prompt user if closed\n * @param issue - Issue to validate\n * @throws LinearServiceError if user cancels due to closed issue\n */\n public async validateIssueState(issue: Issue): Promise<void> {\n if (issue.state === 'closed') {\n const shouldContinue = await this.prompter(\n `Issue ${issue.number} is closed. Continue anyway?`,\n )\n\n if (!shouldContinue) {\n throw new LinearServiceError('INVALID_STATE', 'User cancelled due to closed issue')\n }\n }\n }\n\n /**\n * Create a new Linear issue\n * @param title - Issue title\n * @param body - Issue description (markdown)\n * @param _repository - Repository (unused for Linear)\n * @param labels - Optional label names\n * @returns Created issue identifier and URL\n * @throws LinearServiceError if teamId not configured or creation fails\n */\n public async createIssue(\n title: string,\n body: string,\n _repository?: string,\n labels?: string[],\n ): Promise<{ number: string | number; url: string }> {\n // Require teamId configuration\n if (!this.config.teamId) {\n throw new LinearServiceError(\n 'INVALID_STATE',\n 'Linear teamId not configured. Run `il init` to configure Linear settings.',\n )\n }\n\n logger.info(`Creating Linear issue in team ${this.config.teamId}: ${title}`)\n\n const result = await createLinearIssue(title, body, this.config.teamId, labels)\n\n return {\n number: result.identifier,\n url: result.url,\n }\n }\n\n /**\n * Get the web URL for a Linear issue\n * @param identifier - Linear issue identifier\n * @param _repo - Repository (unused for Linear)\n * @returns Issue URL\n */\n public async getIssueUrl(identifier: string | number, _repo?: string): Promise<string> {\n const issue = await this.fetchIssue(identifier)\n return issue.url\n }\n\n /**\n * Move a Linear issue to \"In Progress\" state\n * @param identifier - Linear issue identifier\n * @throws LinearServiceError if state update fails\n */\n public async moveIssueToInProgress(identifier: string | number): Promise<void> {\n logger.info(`Moving Linear issue ${identifier} to In Progress`)\n await updateLinearIssueState(String(identifier), 'In Progress')\n }\n\n /**\n * Extract issue context for AI prompts\n * @param entity - Issue (Linear doesn't have PRs)\n * @returns Formatted context string\n */\n public extractContext(entity: Issue | PullRequest): string {\n // Linear doesn't have PRs, always an issue\n const issue = entity as Issue\n return `Linear Issue ${issue.number}: ${issue.title}\\nState: ${issue.state}\\n\\n${issue.body}`\n }\n\n /**\n * Map Linear API issue to generic Issue type\n * @param linear - Linear issue from SDK\n * @returns Generic Issue type\n */\n private mapLinearIssueToIssue(linear: LinearIssue): Issue {\n return {\n number: linear.identifier, // Keep as string (e.g., \"ENG-123\")\n title: linear.title,\n body: linear.description ?? '',\n state: linear.state ? (linear.state.toLowerCase().includes('done') || linear.state.toLowerCase().includes('completed') || linear.state.toLowerCase().includes('canceled') ? 'closed' : 'open') : 'open',\n labels: [],\n assignees: [],\n url: linear.url,\n }\n }\n}\n","// IssueTrackerFactory - creates appropriate IssueTracker based on settings\n// Follows pattern from database provider instantiation\n\nimport type { IssueTracker } from './IssueTracker.js'\nimport { GitHubService } from './GitHubService.js'\nimport { LinearService, type LinearServiceConfig } from './LinearService.js'\nimport type { IloomSettings } from './SettingsManager.js'\nimport { getLogger } from '../utils/logger-context.js'\n\nexport type IssueTrackerProviderType = 'github' | 'linear'\n\n/**\n * Factory for creating IssueTracker instances based on settings\n * Provides a single point of provider instantiation\n *\n * Usage:\n * const tracker = IssueTrackerFactory.create(settings, { useClaude: true })\n * const issue = await tracker.fetchIssue(123)\n */\nexport class IssueTrackerFactory {\n\t/**\n\t * Create an IssueTracker instance based on settings configuration\n\t * Defaults to GitHub if no provider specified\n\t *\n\t * @param settings - iloom settings containing issueManagement.provider\n\t * @returns IssueTracker instance configured for the specified provider\n\t * @throws Error if provider type is not supported\n\t */\n\tstatic create(settings: IloomSettings): IssueTracker {\n\t\tconst provider = settings.issueManagement?.provider ?? 'github'\n\n\t\tgetLogger().debug(`IssueTrackerFactory: Creating tracker for provider \"${provider}\"`)\n\t\tgetLogger().debug(`IssueTrackerFactory: issueManagement settings:`, JSON.stringify(settings.issueManagement, null, 2))\n\n\t\tswitch (provider) {\n\t\t\tcase 'github':\n\t\t\t\tgetLogger().debug('IssueTrackerFactory: Creating GitHubService')\n\t\t\t\treturn new GitHubService()\n\t\t\tcase 'linear': {\n\t\t\t\tconst linearSettings = settings.issueManagement?.linear\n\t\t\t\tconst linearConfig: LinearServiceConfig = {}\n\n\t\t\t\tif (linearSettings?.teamId) {\n\t\t\t\t\tlinearConfig.teamId = linearSettings.teamId\n\t\t\t\t}\n\t\t\t\tif (linearSettings?.branchFormat) {\n\t\t\t\t\tlinearConfig.branchFormat = linearSettings.branchFormat\n\t\t\t\t}\n\n\t\t\t\tgetLogger().debug(`IssueTrackerFactory: Creating LinearService with config:`, JSON.stringify(linearConfig, null, 2))\n\t\t\t\treturn new LinearService(linearConfig)\n\t\t\t}\n\t\t\tdefault:\n\t\t\t\tthrow new Error(`Unsupported issue tracker provider: ${provider}`)\n\t\t}\n\t}\n\n\t/**\n\t * Get the configured provider name from settings\n\t * Defaults to 'github' if not configured\n\t *\n\t * @param settings - iloom settings\n\t * @returns Provider type string\n\t */\n\tstatic getProviderName(settings: IloomSettings): IssueTrackerProviderType {\n\t\treturn (settings.issueManagement?.provider ?? 'github') as IssueTrackerProviderType\n\t}\n}\n","import os from 'os'\nimport path from 'path'\nimport fs from 'fs-extra'\nimport { logger } from './logger.js'\n\n/**\n * FirstRunManager: Detect and track first-time spin usage via marker file\n *\n * Follows the same pattern as UpdateNotifier for file-based state tracking\n * in ~/.config/iloom-ai/ directory.\n *\n * Also supports project-level tracking for config wizard completion using\n * individual marker files per project in ~/.config/iloom-ai/projects/\n */\nexport class FirstRunManager {\n\tprivate markerFilePath: string\n\tprivate configDir: string\n\n\tconstructor(feature: string = 'spin') {\n\t\tthis.configDir = path.join(os.homedir(), '.config', 'iloom-ai')\n\t\tthis.markerFilePath = path.join(this.configDir, `${feature}-first-run`)\n\t\tlogger.debug('FirstRunManager initialized', {\n\t\t\tfeature,\n\t\t\tmarkerFilePath: this.markerFilePath\n\t\t})\n\t}\n\n\t/**\n\t * Get the directory for project marker files\n\t */\n\tprivate getProjectsDir(): string {\n\t\treturn path.join(this.configDir, 'projects')\n\t}\n\n\t/**\n\t * Resolve symlinks in project path to get canonical path\n\t * Falls back to original path on errors (broken symlinks, permissions, etc.)\n\t */\n\tprivate async resolveProjectPath(projectPath: string): Promise<string> {\n\t\ttry {\n\t\t\treturn await fs.realpath(projectPath)\n\t\t} catch {\n\t\t\tlogger.debug('resolveProjectPath: Failed to resolve symlink, using original path', { projectPath })\n\t\t\treturn projectPath\n\t\t}\n\t}\n\n\t/**\n\t * Convert a project path to a readable filename\n\t * /Users/adam/Projects/my-app -> Users__adam__Projects__my-app\n\t */\n\tprivate projectPathToFileName(projectPath: string): string {\n\t\tconst normalized = path.normalize(projectPath)\n\t\treturn normalized.replace(/^[/\\\\]+/, '').replace(/[/\\\\]+/g, '__')\n\t}\n\n\t/**\n\t * Get full path to a project's marker file\n\t */\n\tprivate getProjectMarkerPath(projectPath: string): string {\n\t\treturn path.join(this.getProjectsDir(), this.projectPathToFileName(projectPath))\n\t}\n\n\t/**\n\t * Extract project name from path\n\t */\n\tprivate getProjectName(projectPath: string): string {\n\t\treturn path.basename(projectPath)\n\t}\n\n\t/**\n\t * Check if a project has been configured\n\t * Returns true if project marker file exists\n\t */\n\tasync isProjectConfigured(projectPath?: string): Promise<boolean> {\n\t\tconst inputPath = projectPath ?? process.cwd()\n\t\tconst resolvedPath = await this.resolveProjectPath(inputPath)\n\t\tconst markerPath = this.getProjectMarkerPath(resolvedPath)\n\t\tlogger.debug('isProjectConfigured: Checking for marker file', { markerPath })\n\t\ttry {\n\t\t\tconst exists = await fs.pathExists(markerPath)\n\t\t\tlogger.debug(`isProjectConfigured: Marker file exists=${exists}`)\n\t\t\treturn exists\n\t\t} catch (error) {\n\t\t\t// On error, treat as not configured to allow wizard to run\n\t\t\tlogger.debug(`isProjectConfigured: Error checking marker file, treating as not configured: ${error}`)\n\t\t\treturn false\n\t\t}\n\t}\n\n\t/**\n\t * Check if a project has local .iloom settings files\n\t * Returns true if .iloom/settings.json or .iloom/settings.local.json exists with content\n\t */\n\tprivate async hasLocalSettings(projectPath: string): Promise<boolean> {\n\t\tconst iloomDir = path.join(projectPath, '.iloom')\n\t\tconst settingsPath = path.join(iloomDir, 'settings.json')\n\t\tconst settingsLocalPath = path.join(iloomDir, 'settings.local.json')\n\n\t\tconst hasSettings = await this.hasNonEmptySettingsFile(settingsPath)\n\t\tconst hasLocalSettings = await this.hasNonEmptySettingsFile(settingsLocalPath)\n\n\t\treturn hasSettings || hasLocalSettings\n\t}\n\n\t/**\n\t * Check if a settings file exists and has non-empty content\n\t */\n\tprivate async hasNonEmptySettingsFile(filePath: string): Promise<boolean> {\n\t\ttry {\n\t\t\tconst exists = await fs.pathExists(filePath)\n\t\t\tif (!exists) return false\n\t\t\tconst content = await fs.readFile(filePath, 'utf8')\n\t\t\tconst parsed = JSON.parse(content)\n\t\t\treturn Object.keys(parsed).length > 0\n\t\t} catch {\n\t\t\treturn false\n\t\t}\n\t}\n\n\t/**\n\t * Fixup legacy projects that have local config but lack the global marker file\n\t * This handles projects configured before global project tracking was implemented\n\t *\n\t * Returns object with:\n\t * - isConfigured: true if project is configured (either already had marker or fixup created one)\n\t * - wasFixedUp: true if fixup was performed (marker was created by this call)\n\t *\n\t * This combined return avoids duplicate isProjectConfigured() calls in needsFirstRunSetup()\n\t */\n\tasync fixupLegacyProject(projectPath?: string): Promise<{ isConfigured: boolean; wasFixedUp: boolean }> {\n\t\tconst inputPath = projectPath ?? process.cwd()\n\t\tconst resolvedPath = await this.resolveProjectPath(inputPath)\n\t\tlogger.debug('fixupLegacyProject: Checking for legacy project', { projectPath: resolvedPath })\n\n\t\t// Check if already has global marker - no fixup needed, but project IS configured\n\t\tconst hasMarker = await this.isProjectConfigured(resolvedPath)\n\t\tif (hasMarker) {\n\t\t\tlogger.debug('fixupLegacyProject: Project already has global marker')\n\t\t\treturn { isConfigured: true, wasFixedUp: false }\n\t\t}\n\n\t\t// Check if has local settings files - this indicates a legacy configured project\n\t\tconst hasLocal = await this.hasLocalSettings(resolvedPath)\n\t\tif (!hasLocal) {\n\t\t\tlogger.debug('fixupLegacyProject: No local settings found, not a legacy project')\n\t\t\treturn { isConfigured: false, wasFixedUp: false }\n\t\t}\n\n\t\t// Legacy project found - create the missing global marker\n\t\tlogger.debug('fixupLegacyProject: Legacy project detected, creating global marker')\n\t\tawait this.markProjectAsConfigured(resolvedPath)\n\t\treturn { isConfigured: true, wasFixedUp: true }\n\t}\n\n\t/**\n\t * Mark a project as configured\n\t * Creates a marker file with project metadata\n\t */\n\tasync markProjectAsConfigured(projectPath?: string): Promise<void> {\n\t\tconst inputPath = projectPath ?? process.cwd()\n\t\tconst resolvedPath = await this.resolveProjectPath(inputPath)\n\t\tconst markerPath = this.getProjectMarkerPath(resolvedPath)\n\t\tlogger.debug('markProjectAsConfigured: Creating marker file', { markerPath })\n\t\ttry {\n\t\t\tawait fs.ensureDir(this.getProjectsDir())\n\t\t\tconst markerContent = {\n\t\t\t\tconfiguredAt: new Date().toISOString(),\n\t\t\t\tprojectPath: resolvedPath,\n\t\t\t\tprojectName: this.getProjectName(resolvedPath)\n\t\t\t}\n\t\t\tawait fs.writeFile(markerPath, JSON.stringify(markerContent, null, 2), 'utf8')\n\t\t\tlogger.debug('markProjectAsConfigured: Marker file created successfully')\n\t\t} catch (error) {\n\t\t\t// Don't throw on errors - just log debug message\n\t\t\tlogger.debug(`markProjectAsConfigured: Failed to create marker file: ${error}`)\n\t\t}\n\t}\n\n\t/**\n\t * Check if this is the first run of the feature\n\t * Returns true if marker file doesn't exist\n\t * Handles errors gracefully by returning true (treat as first-run on error)\n\t */\n\tasync isFirstRun(): Promise<boolean> {\n\t\tlogger.debug('isFirstRun: Checking for marker file', { markerFilePath: this.markerFilePath })\n\t\ttry {\n\t\t\tconst exists = await fs.pathExists(this.markerFilePath)\n\t\t\tlogger.debug(`isFirstRun: Marker file exists=${exists}`)\n\t\t\treturn !exists\n\t\t} catch (error) {\n\t\t\t// On error, gracefully degrade by treating as first-run\n\t\t\tlogger.debug(`isFirstRun: Error checking marker file, treating as first-run: ${error}`)\n\t\t\treturn true\n\t\t}\n\t}\n\n\t/**\n\t * Mark the feature as having been run\n\t * Creates the marker file in config directory\n\t * Handles errors gracefully without throwing\n\t */\n\tasync markAsRun(): Promise<void> {\n\t\tlogger.debug('markAsRun: Attempting to create marker file', { markerFilePath: this.markerFilePath })\n\t\ttry {\n\t\t\t// Ensure directory exists\n\t\t\tconst configDir = path.dirname(this.markerFilePath)\n\t\t\tlogger.debug(`markAsRun: Ensuring config directory exists: ${configDir}`)\n\t\t\tawait fs.ensureDir(configDir)\n\n\t\t\t// Write marker file with timestamp for debugging\n\t\t\tconst markerContent = {\n\t\t\t\tfirstRun: new Date().toISOString(),\n\t\t\t}\n\t\t\tawait fs.writeFile(this.markerFilePath, JSON.stringify(markerContent, null, 2), 'utf8')\n\t\t\tlogger.debug('markAsRun: Marker file created successfully')\n\t\t} catch (error) {\n\t\t\t// Don't throw on errors - just log debug message\n\t\t\t// Failing to write marker shouldn't break the workflow\n\t\t\tlogger.debug(`markAsRun: Failed to create marker file: ${error}`)\n\t\t}\n\t}\n}\n","import path from 'path'\nimport os from 'os'\nimport { getRepoInfo } from './github.js'\nimport { logger } from './logger.js'\nimport type { IloomSettings } from '../lib/SettingsManager.js'\nimport type { LoomMetadata } from '../lib/MetadataManager.js'\n\n/**\n * Generate MCP configuration for issue management\n * Uses a single server that can handle both issues and pull requests\n * Returns array of MCP server config objects\n * @param contextType - Optional context type (issue or pr)\n * @param repo - Optional repo in \"owner/repo\" format. If not provided, will auto-detect from git.\n * @param provider - Issue management provider (default: 'github')\n * @param settings - Optional settings to extract Linear API token from\n * @param draftPrNumber - Optional draft PR number for github-draft-pr mode (routes comments to PR)\n */\nexport async function generateIssueManagementMcpConfig(\n\tcontextType?: 'issue' | 'pr',\n\trepo?: string,\n\tprovider: 'github' | 'linear' = 'github',\n\tsettings?: IloomSettings,\n\tdraftPrNumber?: number\n): Promise<Record<string, unknown>[]> {\n\t// When draftPrNumber is provided (github-draft-pr mode), force contextType to 'pr'\n\t// This ensures agents route comments to the draft PR instead of the issue\n\tconst effectiveContextType = draftPrNumber ? 'pr' : contextType\n\n\t// Build provider-specific environment variables\n\tlet envVars: Record<string, string> = {\n\t\tISSUE_PROVIDER: provider,\n\t}\n\n\t// Add draft PR number to env vars if provided\n\tif (draftPrNumber) {\n\t\tenvVars.DRAFT_PR_NUMBER = String(draftPrNumber)\n\t}\n\n\tif (provider === 'github') {\n\t\t// Get repository information for GitHub - either from provided repo string or auto-detect\n\t\tlet owner: string\n\t\tlet name: string\n\n\t\tif (repo) {\n\t\t\tconst parts = repo.split('/')\n\t\t\tif (parts.length !== 2 || !parts[0] || !parts[1]) {\n\t\t\t\tthrow new Error(`Invalid repo format: ${repo}. Expected \"owner/repo\"`)\n\t\t\t}\n\t\t\towner = parts[0]\n\t\t\tname = parts[1]\n\t\t} else {\n\t\t\tconst repoInfo = await getRepoInfo()\n\t\t\towner = repoInfo.owner\n\t\t\tname = repoInfo.name\n\t\t}\n\n\t\t// Map logical types to GitHub's webhook event names (handle GitHub's naming quirk here)\n\t\t// Use effectiveContextType which may be overridden by draftPrNumber\n\t\tconst githubEventName = effectiveContextType === 'issue' ? 'issues' : effectiveContextType === 'pr' ? 'pull_request' : undefined\n\n\t\tenvVars = {\n\t\t\t...envVars,\n\t\t\tREPO_OWNER: owner,\n\t\t\tREPO_NAME: name,\n\t\t\tGITHUB_API_URL: 'https://api.github.com/',\n\t\t\t...(githubEventName && { GITHUB_EVENT_NAME: githubEventName }),\n\t\t}\n\n\t\tlogger.debug('Generated MCP config for GitHub issue management', {\n\t\t\tprovider,\n\t\t\trepoOwner: owner,\n\t\t\trepoName: name,\n\t\t\tcontextType: effectiveContextType ?? 'auto-detect',\n\t\t\tgithubEventName: githubEventName ?? 'auto-detect',\n\t\t\tdraftPrNumber: draftPrNumber ?? undefined,\n\t\t})\n\t} else {\n\t\t// Linear needs API token passed through\n\t\tconst apiToken = settings?.issueManagement?.linear?.apiToken ?? process.env.LINEAR_API_TOKEN\n\n\t\tif (apiToken) {\n\t\t\tenvVars.LINEAR_API_TOKEN = apiToken\n\t\t}\n\n\t\tlogger.debug('Generated MCP config for Linear issue management', {\n\t\t\tprovider,\n\t\t\thasApiToken: !!apiToken,\n\t\t\tcontextType: contextType ?? 'auto-detect',\n\t\t})\n\t}\n\n\t// Generate single MCP server config\n\tconst mcpServerConfig = {\n\t\tmcpServers: {\n\t\t\tissue_management: {\n\t\t\t\ttransport: 'stdio',\n\t\t\t\tcommand: 'node',\n\t\t\t\targs: [path.join(path.dirname(new globalThis.URL(import.meta.url).pathname), '../dist/mcp/issue-management-server.js')],\n\t\t\t\tenv: envVars,\n\t\t\t},\n\t\t},\n\t}\n\n\treturn [mcpServerConfig]\n}\n\n/**\n * Reuse MetadataManager.slugifyPath() algorithm for recap file naming\n *\n * Algorithm:\n * 1. Trim trailing slashes\n * 2. Replace all path separators (/ or \\) with ___ (triple underscore)\n * 3. Replace any other non-alphanumeric characters (except _ and -) with -\n * 4. Append .json\n */\nfunction slugifyPath(loomPath: string): string {\n\tlet slug = loomPath.replace(/[/\\\\]+$/, '')\n\tslug = slug.replace(/[/\\\\]/g, '___')\n\tslug = slug.replace(/[^a-zA-Z0-9_-]/g, '-')\n\treturn `${slug}.json`\n}\n\n/**\n * Generate MCP configuration for recap server\n *\n * The recap server captures session context (goal, decisions, insights, risks, assumptions)\n * for the VS Code Loom Context Panel.\n *\n * @param loomPath - Absolute path to the loom workspace\n * @param loomMetadata - The loom metadata object (will be stringified as JSON)\n */\nexport function generateRecapMcpConfig(\n\tloomPath: string,\n\tloomMetadata: LoomMetadata\n): Record<string, unknown>[] {\n\t// Compute recap file path using slugifyPath algorithm (same as MetadataManager)\n\tconst recapsDir = path.join(os.homedir(), '.config', 'iloom-ai', 'recaps')\n\tconst recapFilePath = path.join(recapsDir, slugifyPath(loomPath))\n\n\t// Pass both env vars:\n\t// - RECAP_FILE_PATH: where to read/write recap data\n\t// - LOOM_METADATA_JSON: stringified loom metadata (parsed by MCP using LoomMetadata type)\n\tconst envVars = {\n\t\tRECAP_FILE_PATH: recapFilePath,\n\t\tLOOM_METADATA_JSON: JSON.stringify(loomMetadata),\n\t}\n\n\tlogger.debug('Generated MCP config for recap server', {\n\t\tloomPath,\n\t\trecapFilePath,\n\t\tloomMetadataDescription: loomMetadata.description,\n\t})\n\n\treturn [\n\t\t{\n\t\t\tmcpServers: {\n\t\t\t\trecap: {\n\t\t\t\t\ttransport: 'stdio',\n\t\t\t\t\tcommand: 'node',\n\t\t\t\t\targs: [\n\t\t\t\t\t\tpath.join(\n\t\t\t\t\t\t\tpath.dirname(new globalThis.URL(import.meta.url).pathname),\n\t\t\t\t\t\t\t'../dist/mcp/recap-server.js'\n\t\t\t\t\t\t),\n\t\t\t\t\t],\n\t\t\t\t\tenv: envVars,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t]\n}"],"mappings":"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","names":["path","os","path","os"]}
|