@snipcodeit/mgw 0.2.2 → 0.4.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 +55 -5
- package/bin/mgw-install.cjs +121 -24
- package/commands/board/configure.md +205 -0
- package/commands/board/create.md +688 -0
- package/commands/board/show.md +221 -0
- package/commands/board/sync.md +461 -0
- package/commands/board/views.md +253 -0
- package/commands/board.md +23 -1543
- package/commands/context.md +183 -0
- package/commands/handoff.md +169 -0
- package/commands/issue.md +62 -0
- package/commands/milestone.md +42 -43
- package/commands/project.md +19 -0
- package/commands/review.md +222 -42
- package/commands/run/execute.md +820 -0
- package/commands/run/pr-create.md +324 -0
- package/commands/run/triage.md +510 -0
- package/commands/run/worktree.md +95 -0
- package/commands/run.md +23 -1547
- package/commands/sync.md +69 -0
- package/commands/workflows/gsd.md +1 -13
- package/dist/bin/mgw.cjs +107 -15
- package/dist/{index-BiwU0uWA.cjs → index-B-_JvYpz.cjs} +885 -69
- package/dist/lib/index.cjs +653 -155
- package/package.json +5 -2
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var require$$
|
|
3
|
+
var require$$2 = require('fs');
|
|
4
4
|
var require$$1 = require('path');
|
|
5
|
-
var require$$0
|
|
6
|
-
var require$$
|
|
5
|
+
var require$$0 = require('child_process');
|
|
6
|
+
var require$$3 = require('os');
|
|
7
|
+
var require$$0$1 = require('events');
|
|
7
8
|
|
|
8
9
|
function getDefaultExportFromCjs(x) {
|
|
9
10
|
return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, "default") ? x["default"] : x;
|
|
@@ -15,7 +16,7 @@ var hasRequiredState;
|
|
|
15
16
|
function requireState () {
|
|
16
17
|
if (hasRequiredState) return state;
|
|
17
18
|
hasRequiredState = 1;
|
|
18
|
-
const fs = require$$
|
|
19
|
+
const fs = require$$2;
|
|
19
20
|
const path = require$$1;
|
|
20
21
|
function getMgwDir() {
|
|
21
22
|
return path.join(process.cwd(), ".mgw");
|
|
@@ -84,17 +85,21 @@ function requireState () {
|
|
|
84
85
|
return existing;
|
|
85
86
|
}
|
|
86
87
|
function migrateProjectState() {
|
|
88
|
+
const warnings = [];
|
|
87
89
|
const existing = loadProjectState();
|
|
88
|
-
if (!existing) return null;
|
|
90
|
+
if (!existing) return { state: null, warnings: [] };
|
|
89
91
|
let changed = false;
|
|
90
92
|
if (!existing.hasOwnProperty("active_gsd_milestone")) {
|
|
91
93
|
existing.active_gsd_milestone = null;
|
|
92
94
|
changed = true;
|
|
95
|
+
warnings.push("migration: added active_gsd_milestone field");
|
|
93
96
|
}
|
|
94
97
|
for (const m of existing.milestones || []) {
|
|
98
|
+
const mLabel = m.title || m.gsd_milestone_id || "unnamed";
|
|
95
99
|
if (!m.hasOwnProperty("gsd_milestone_id")) {
|
|
96
100
|
m.gsd_milestone_id = null;
|
|
97
101
|
changed = true;
|
|
102
|
+
warnings.push(`migration: added gsd_milestone_id to milestone "${mLabel}"`);
|
|
98
103
|
}
|
|
99
104
|
if (!m.hasOwnProperty("gsd_state")) {
|
|
100
105
|
m.gsd_state = null;
|
|
@@ -113,8 +118,9 @@ function requireState () {
|
|
|
113
118
|
let entries;
|
|
114
119
|
try {
|
|
115
120
|
entries = fs.readdirSync(activeDir);
|
|
116
|
-
} catch {
|
|
121
|
+
} catch (err) {
|
|
117
122
|
entries = [];
|
|
123
|
+
warnings.push(`migration: could not read active dir: ${err.message}`);
|
|
118
124
|
}
|
|
119
125
|
for (const file of entries) {
|
|
120
126
|
if (!file.endsWith(".json")) continue;
|
|
@@ -122,7 +128,8 @@ function requireState () {
|
|
|
122
128
|
let issueState;
|
|
123
129
|
try {
|
|
124
130
|
issueState = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
125
|
-
} catch {
|
|
131
|
+
} catch (err) {
|
|
132
|
+
warnings.push(`migration: skipping unreadable ${file}: ${err.message}`);
|
|
126
133
|
continue;
|
|
127
134
|
}
|
|
128
135
|
let issueChanged = false;
|
|
@@ -137,12 +144,13 @@ function requireState () {
|
|
|
137
144
|
if (issueChanged) {
|
|
138
145
|
try {
|
|
139
146
|
fs.writeFileSync(filePath, JSON.stringify(issueState, null, 2), "utf-8");
|
|
140
|
-
} catch {
|
|
147
|
+
} catch (err) {
|
|
148
|
+
warnings.push(`migration: failed to write ${file}: ${err.message}`);
|
|
141
149
|
}
|
|
142
150
|
}
|
|
143
151
|
}
|
|
144
152
|
}
|
|
145
|
-
return existing;
|
|
153
|
+
return { state: existing, warnings };
|
|
146
154
|
}
|
|
147
155
|
function resolveActiveMilestoneIndex(state) {
|
|
148
156
|
if (!state) return -1;
|
|
@@ -157,6 +165,159 @@ function requireState () {
|
|
|
157
165
|
}
|
|
158
166
|
return -1;
|
|
159
167
|
}
|
|
168
|
+
const VALID_LINK_TYPES = /* @__PURE__ */ new Set(["related", "implements", "tracks", "maps-to", "blocked-by"]);
|
|
169
|
+
function loadCrossRefs() {
|
|
170
|
+
const filePath = path.join(getMgwDir(), "cross-refs.json");
|
|
171
|
+
const warnings = [];
|
|
172
|
+
if (!fs.existsSync(filePath)) {
|
|
173
|
+
return { links: [], warnings: [] };
|
|
174
|
+
}
|
|
175
|
+
let raw;
|
|
176
|
+
try {
|
|
177
|
+
raw = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
178
|
+
} catch (err) {
|
|
179
|
+
return { links: [], warnings: [`cross-refs.json parse error: ${err.message}`] };
|
|
180
|
+
}
|
|
181
|
+
if (!raw || !Array.isArray(raw.links)) {
|
|
182
|
+
return { links: [], warnings: ["cross-refs.json missing links array"] };
|
|
183
|
+
}
|
|
184
|
+
const validLinks = [];
|
|
185
|
+
for (let i = 0; i < raw.links.length; i++) {
|
|
186
|
+
const link = raw.links[i];
|
|
187
|
+
const issues = [];
|
|
188
|
+
if (!link || typeof link !== "object") {
|
|
189
|
+
warnings.push(`cross-refs link[${i}]: not an object, skipping`);
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
if (typeof link.a !== "string" || !link.a) {
|
|
193
|
+
issues.push('missing "a"');
|
|
194
|
+
}
|
|
195
|
+
if (typeof link.b !== "string" || !link.b) {
|
|
196
|
+
issues.push('missing "b"');
|
|
197
|
+
}
|
|
198
|
+
if (link.type && !VALID_LINK_TYPES.has(link.type)) {
|
|
199
|
+
issues.push(`unknown type "${link.type}"`);
|
|
200
|
+
}
|
|
201
|
+
if (issues.length > 0) {
|
|
202
|
+
warnings.push(`cross-refs link[${i}]: ${issues.join(", ")}, skipping`);
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
validLinks.push(link);
|
|
206
|
+
}
|
|
207
|
+
return { links: validLinks, warnings };
|
|
208
|
+
}
|
|
209
|
+
function parseDependencies(body) {
|
|
210
|
+
if (!body || typeof body !== "string") return [];
|
|
211
|
+
const deps = /* @__PURE__ */ new Set();
|
|
212
|
+
const linePattern = /(?:depends[\s-]*on|blocked[\s-]*by)[:\s]+([^\n]+)/gi;
|
|
213
|
+
let match;
|
|
214
|
+
while ((match = linePattern.exec(body)) !== null) {
|
|
215
|
+
const refs = match[1];
|
|
216
|
+
const numPattern = /#(\d+)/g;
|
|
217
|
+
let numMatch;
|
|
218
|
+
while ((numMatch = numPattern.exec(refs)) !== null) {
|
|
219
|
+
deps.add(parseInt(numMatch[1], 10));
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return Array.from(deps).sort((a, b) => a - b);
|
|
223
|
+
}
|
|
224
|
+
function storeDependencies(issueNumber, dependsOn) {
|
|
225
|
+
if (!dependsOn || dependsOn.length === 0) return { added: 0, existing: 0 };
|
|
226
|
+
const crossRefsPath = path.join(getMgwDir(), "cross-refs.json");
|
|
227
|
+
let crossRefs = { links: [] };
|
|
228
|
+
if (fs.existsSync(crossRefsPath)) {
|
|
229
|
+
try {
|
|
230
|
+
crossRefs = JSON.parse(fs.readFileSync(crossRefsPath, "utf-8"));
|
|
231
|
+
if (!Array.isArray(crossRefs.links)) crossRefs.links = [];
|
|
232
|
+
} catch {
|
|
233
|
+
crossRefs = { links: [] };
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
let added = 0;
|
|
237
|
+
let existing = 0;
|
|
238
|
+
const issueRef = `#${issueNumber}`;
|
|
239
|
+
for (const dep of dependsOn) {
|
|
240
|
+
const depRef = `#${dep}`;
|
|
241
|
+
const alreadyExists = crossRefs.links.some(
|
|
242
|
+
(l) => l.a === issueRef && l.b === depRef && l.type === "blocked-by"
|
|
243
|
+
);
|
|
244
|
+
if (alreadyExists) {
|
|
245
|
+
existing++;
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
crossRefs.links.push({
|
|
249
|
+
a: issueRef,
|
|
250
|
+
b: depRef,
|
|
251
|
+
type: "blocked-by",
|
|
252
|
+
created: (/* @__PURE__ */ new Date()).toISOString()
|
|
253
|
+
});
|
|
254
|
+
added++;
|
|
255
|
+
}
|
|
256
|
+
if (added > 0) {
|
|
257
|
+
const mgwDir = getMgwDir();
|
|
258
|
+
if (!fs.existsSync(mgwDir)) {
|
|
259
|
+
fs.mkdirSync(mgwDir, { recursive: true });
|
|
260
|
+
}
|
|
261
|
+
fs.writeFileSync(crossRefsPath, JSON.stringify(crossRefs, null, 2), "utf-8");
|
|
262
|
+
}
|
|
263
|
+
return { added, existing };
|
|
264
|
+
}
|
|
265
|
+
function topologicalSort(issues, links) {
|
|
266
|
+
if (!issues || issues.length === 0) return [];
|
|
267
|
+
if (!links || links.length === 0) return [...issues];
|
|
268
|
+
const deps = /* @__PURE__ */ new Map();
|
|
269
|
+
const issueSet = new Set(issues.map((i) => i.number));
|
|
270
|
+
for (const issue of issues) {
|
|
271
|
+
deps.set(issue.number, /* @__PURE__ */ new Set());
|
|
272
|
+
}
|
|
273
|
+
for (const link of links) {
|
|
274
|
+
if (link.type !== "blocked-by") continue;
|
|
275
|
+
const aMatch = String(link.a).match(/#(\d+)/);
|
|
276
|
+
const bMatch = String(link.b).match(/#(\d+)/);
|
|
277
|
+
if (!aMatch || !bMatch) continue;
|
|
278
|
+
const a = parseInt(aMatch[1], 10);
|
|
279
|
+
const b = parseInt(bMatch[1], 10);
|
|
280
|
+
if (issueSet.has(a) && issueSet.has(b) && deps.has(a)) {
|
|
281
|
+
deps.get(a).add(b);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
const inDegree = /* @__PURE__ */ new Map();
|
|
285
|
+
for (const issue of issues) {
|
|
286
|
+
inDegree.set(issue.number, deps.get(issue.number).size);
|
|
287
|
+
}
|
|
288
|
+
const queue = [];
|
|
289
|
+
for (const [num, degree] of inDegree) {
|
|
290
|
+
if (degree === 0) queue.push(num);
|
|
291
|
+
}
|
|
292
|
+
const issueMap = new Map(issues.map((i) => [i.number, i]));
|
|
293
|
+
const sorted = [];
|
|
294
|
+
while (queue.length > 0) {
|
|
295
|
+
queue.sort((a, b) => {
|
|
296
|
+
const idxA = issues.findIndex((i) => i.number === a);
|
|
297
|
+
const idxB = issues.findIndex((i) => i.number === b);
|
|
298
|
+
return idxA - idxB;
|
|
299
|
+
});
|
|
300
|
+
const num = queue.shift();
|
|
301
|
+
sorted.push(issueMap.get(num));
|
|
302
|
+
for (const [dependent, depSet] of deps) {
|
|
303
|
+
if (depSet.has(num)) {
|
|
304
|
+
depSet.delete(num);
|
|
305
|
+
inDegree.set(dependent, inDegree.get(dependent) - 1);
|
|
306
|
+
if (inDegree.get(dependent) === 0) {
|
|
307
|
+
queue.push(dependent);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
if (sorted.length < issues.length) {
|
|
313
|
+
for (const issue of issues) {
|
|
314
|
+
if (!sorted.includes(issue)) {
|
|
315
|
+
sorted.push(issue);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return sorted;
|
|
320
|
+
}
|
|
160
321
|
state = {
|
|
161
322
|
getMgwDir,
|
|
162
323
|
getActiveDir,
|
|
@@ -166,34 +327,312 @@ function requireState () {
|
|
|
166
327
|
loadActiveIssue,
|
|
167
328
|
mergeProjectState,
|
|
168
329
|
migrateProjectState,
|
|
169
|
-
resolveActiveMilestoneIndex
|
|
330
|
+
resolveActiveMilestoneIndex,
|
|
331
|
+
loadCrossRefs,
|
|
332
|
+
VALID_LINK_TYPES,
|
|
333
|
+
parseDependencies,
|
|
334
|
+
storeDependencies,
|
|
335
|
+
topologicalSort
|
|
170
336
|
};
|
|
171
337
|
return state;
|
|
172
338
|
}
|
|
173
339
|
|
|
340
|
+
var errors;
|
|
341
|
+
var hasRequiredErrors;
|
|
342
|
+
|
|
343
|
+
function requireErrors () {
|
|
344
|
+
if (hasRequiredErrors) return errors;
|
|
345
|
+
hasRequiredErrors = 1;
|
|
346
|
+
class MgwError extends Error {
|
|
347
|
+
/**
|
|
348
|
+
* @param {string} message
|
|
349
|
+
* @param {object} [opts]
|
|
350
|
+
* @param {string} [opts.code] - Machine-readable error code
|
|
351
|
+
* @param {string} [opts.stage] - Pipeline stage where error occurred
|
|
352
|
+
* @param {number} [opts.issueNumber] - Related GitHub issue number
|
|
353
|
+
* @param {Error} [opts.cause] - Original error
|
|
354
|
+
*/
|
|
355
|
+
constructor(message, opts) {
|
|
356
|
+
super(message);
|
|
357
|
+
this.name = "MgwError";
|
|
358
|
+
const o = opts || {};
|
|
359
|
+
this.code = o.code || "MGW_ERROR";
|
|
360
|
+
this.stage = o.stage || null;
|
|
361
|
+
this.issueNumber = o.issueNumber || null;
|
|
362
|
+
if (o.cause) this.cause = o.cause;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
class GitHubApiError extends MgwError {
|
|
366
|
+
/**
|
|
367
|
+
* @param {string} message
|
|
368
|
+
* @param {object} [opts]
|
|
369
|
+
* @param {number} [opts.status] - HTTP status code
|
|
370
|
+
* @param {string} [opts.endpoint] - API endpoint or gh subcommand
|
|
371
|
+
*/
|
|
372
|
+
constructor(message, opts) {
|
|
373
|
+
const o = opts || {};
|
|
374
|
+
super(message, { code: "GITHUB_API_ERROR", ...o });
|
|
375
|
+
this.name = "GitHubApiError";
|
|
376
|
+
this.status = o.status || null;
|
|
377
|
+
this.endpoint = o.endpoint || null;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
class GsdToolError extends MgwError {
|
|
381
|
+
/**
|
|
382
|
+
* @param {string} message
|
|
383
|
+
* @param {object} [opts]
|
|
384
|
+
* @param {string} [opts.command] - GSD subcommand that failed
|
|
385
|
+
*/
|
|
386
|
+
constructor(message, opts) {
|
|
387
|
+
const o = opts || {};
|
|
388
|
+
super(message, { code: "GSD_TOOL_ERROR", ...o });
|
|
389
|
+
this.name = "GsdToolError";
|
|
390
|
+
this.command = o.command || null;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
class StateError extends MgwError {
|
|
394
|
+
/**
|
|
395
|
+
* @param {string} message
|
|
396
|
+
* @param {object} [opts]
|
|
397
|
+
* @param {string} [opts.filePath] - State file path
|
|
398
|
+
*/
|
|
399
|
+
constructor(message, opts) {
|
|
400
|
+
const o = opts || {};
|
|
401
|
+
super(message, { code: "STATE_ERROR", ...o });
|
|
402
|
+
this.name = "StateError";
|
|
403
|
+
this.filePath = o.filePath || null;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
class TimeoutError extends MgwError {
|
|
407
|
+
/**
|
|
408
|
+
* @param {string} message
|
|
409
|
+
* @param {object} [opts]
|
|
410
|
+
* @param {number} [opts.timeoutMs] - Timeout duration in milliseconds
|
|
411
|
+
* @param {string} [opts.operation] - Description of the timed-out operation
|
|
412
|
+
*/
|
|
413
|
+
constructor(message, opts) {
|
|
414
|
+
const o = opts || {};
|
|
415
|
+
super(message, { code: "TIMEOUT_ERROR", ...o });
|
|
416
|
+
this.name = "TimeoutError";
|
|
417
|
+
this.timeoutMs = o.timeoutMs || null;
|
|
418
|
+
this.operation = o.operation || null;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
class ClaudeNotAvailableError extends MgwError {
|
|
422
|
+
/**
|
|
423
|
+
* @param {string} message
|
|
424
|
+
* @param {object} [opts]
|
|
425
|
+
* @param {'not-installed'|'not-authenticated'|'check-failed'} [opts.reason]
|
|
426
|
+
*/
|
|
427
|
+
constructor(message, opts) {
|
|
428
|
+
const o = opts || {};
|
|
429
|
+
super(message, { code: "CLAUDE_NOT_AVAILABLE", ...o });
|
|
430
|
+
this.name = "ClaudeNotAvailableError";
|
|
431
|
+
this.reason = o.reason || null;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
errors = {
|
|
435
|
+
MgwError,
|
|
436
|
+
GitHubApiError,
|
|
437
|
+
GsdToolError,
|
|
438
|
+
StateError,
|
|
439
|
+
TimeoutError,
|
|
440
|
+
ClaudeNotAvailableError
|
|
441
|
+
};
|
|
442
|
+
return errors;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
var retry;
|
|
446
|
+
var hasRequiredRetry;
|
|
447
|
+
|
|
448
|
+
function requireRetry () {
|
|
449
|
+
if (hasRequiredRetry) return retry;
|
|
450
|
+
hasRequiredRetry = 1;
|
|
451
|
+
const MAX_RETRIES = 3;
|
|
452
|
+
const BACKOFF_BASE_MS = 5e3;
|
|
453
|
+
const BACKOFF_MAX_MS = 3e5;
|
|
454
|
+
const TRANSIENT_STATUS_CODES = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
|
|
455
|
+
const TRANSIENT_MESSAGE_PATTERNS = [
|
|
456
|
+
"network timeout",
|
|
457
|
+
"econnreset",
|
|
458
|
+
"econnrefused",
|
|
459
|
+
"etimedout",
|
|
460
|
+
"socket hang up",
|
|
461
|
+
"worktree lock",
|
|
462
|
+
"model overload",
|
|
463
|
+
"rate limit",
|
|
464
|
+
"too many requests",
|
|
465
|
+
"service unavailable",
|
|
466
|
+
"bad gateway",
|
|
467
|
+
"gateway timeout"
|
|
468
|
+
];
|
|
469
|
+
const NEEDS_INFO_MESSAGE_PATTERNS = [
|
|
470
|
+
"ambiguous",
|
|
471
|
+
"missing required field",
|
|
472
|
+
"contradictory requirements",
|
|
473
|
+
"issue body"
|
|
474
|
+
];
|
|
475
|
+
function classifyFailure(error) {
|
|
476
|
+
if (!error || typeof error !== "object") {
|
|
477
|
+
return { class: "permanent", reason: "no error object provided" };
|
|
478
|
+
}
|
|
479
|
+
const status = error.status;
|
|
480
|
+
const message = (error.message || "").toLowerCase();
|
|
481
|
+
const code = (error.code || "").toLowerCase();
|
|
482
|
+
if (typeof status === "number") {
|
|
483
|
+
if (status === 429) {
|
|
484
|
+
return { class: "transient", reason: "rate limit (HTTP 429)" };
|
|
485
|
+
}
|
|
486
|
+
if (TRANSIENT_STATUS_CODES.has(status)) {
|
|
487
|
+
return { class: "transient", reason: `server error (HTTP ${status})` };
|
|
488
|
+
}
|
|
489
|
+
if (status === 403) {
|
|
490
|
+
return { class: "permanent", reason: "forbidden (HTTP 403 \u2014 non-rate-limit)" };
|
|
491
|
+
}
|
|
492
|
+
if (status >= 400 && status < 500) {
|
|
493
|
+
return { class: "permanent", reason: `client error (HTTP ${status})` };
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
if (code) {
|
|
497
|
+
const networkCodes = /* @__PURE__ */ new Set([
|
|
498
|
+
"econnreset",
|
|
499
|
+
"econnrefused",
|
|
500
|
+
"etimedout",
|
|
501
|
+
"enotfound",
|
|
502
|
+
"epipe"
|
|
503
|
+
]);
|
|
504
|
+
if (networkCodes.has(code)) {
|
|
505
|
+
return { class: "transient", reason: `network error (${code.toUpperCase()})` };
|
|
506
|
+
}
|
|
507
|
+
if (code === "enoent") {
|
|
508
|
+
return { class: "permanent", reason: "file not found (ENOENT) \u2014 GSD tools may be missing" };
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
for (const pattern of TRANSIENT_MESSAGE_PATTERNS) {
|
|
512
|
+
if (message.includes(pattern)) {
|
|
513
|
+
return { class: "transient", reason: `transient condition detected: "${pattern}"` };
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
for (const pattern of NEEDS_INFO_MESSAGE_PATTERNS) {
|
|
517
|
+
if (message.includes(pattern)) {
|
|
518
|
+
return { class: "needs-info", reason: `issue requires clarification: "${pattern}"` };
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
return {
|
|
522
|
+
class: "permanent",
|
|
523
|
+
reason: "unknown error \u2014 classified as permanent to prevent runaway retries"
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
function canRetry(issueState) {
|
|
527
|
+
if (!issueState || typeof issueState !== "object") return false;
|
|
528
|
+
if (issueState.dead_letter === true) return false;
|
|
529
|
+
const count = typeof issueState.retry_count === "number" ? issueState.retry_count : 0;
|
|
530
|
+
return count < MAX_RETRIES;
|
|
531
|
+
}
|
|
532
|
+
function incrementRetry(issueState) {
|
|
533
|
+
const current = typeof issueState.retry_count === "number" ? issueState.retry_count : 0;
|
|
534
|
+
return Object.assign({}, issueState, { retry_count: current + 1 });
|
|
535
|
+
}
|
|
536
|
+
function resetRetryState(issueState) {
|
|
537
|
+
return Object.assign({}, issueState, {
|
|
538
|
+
retry_count: 0,
|
|
539
|
+
last_failure_class: null,
|
|
540
|
+
dead_letter: false
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
function getBackoffMs(retryCount) {
|
|
544
|
+
const count = Math.max(0, Math.floor(retryCount));
|
|
545
|
+
const base = Math.min(BACKOFF_MAX_MS, BACKOFF_BASE_MS * Math.pow(2, count));
|
|
546
|
+
return Math.floor(Math.random() * (base + 1));
|
|
547
|
+
}
|
|
548
|
+
async function withRetry(fn, opts) {
|
|
549
|
+
const o = opts || {};
|
|
550
|
+
const maxAttempts = (typeof o.maxRetries === "number" ? o.maxRetries : MAX_RETRIES) + 1;
|
|
551
|
+
let lastError;
|
|
552
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
553
|
+
try {
|
|
554
|
+
return await fn();
|
|
555
|
+
} catch (err) {
|
|
556
|
+
lastError = err;
|
|
557
|
+
if (attempt >= maxAttempts - 1) break;
|
|
558
|
+
const classification = classifyFailure(err);
|
|
559
|
+
if (classification.class !== "transient") break;
|
|
560
|
+
const delayMs = getBackoffMs(attempt);
|
|
561
|
+
if (delayMs > 0) {
|
|
562
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
throw lastError;
|
|
567
|
+
}
|
|
568
|
+
retry = {
|
|
569
|
+
// Constants
|
|
570
|
+
MAX_RETRIES,
|
|
571
|
+
BACKOFF_BASE_MS,
|
|
572
|
+
BACKOFF_MAX_MS,
|
|
573
|
+
// Core functions
|
|
574
|
+
classifyFailure,
|
|
575
|
+
canRetry,
|
|
576
|
+
incrementRetry,
|
|
577
|
+
resetRetryState,
|
|
578
|
+
getBackoffMs,
|
|
579
|
+
withRetry
|
|
580
|
+
};
|
|
581
|
+
return retry;
|
|
582
|
+
}
|
|
583
|
+
|
|
174
584
|
var github;
|
|
175
585
|
var hasRequiredGithub;
|
|
176
586
|
|
|
177
587
|
function requireGithub () {
|
|
178
588
|
if (hasRequiredGithub) return github;
|
|
179
589
|
hasRequiredGithub = 1;
|
|
180
|
-
const { execSync } = require$$0
|
|
590
|
+
const { execSync } = require$$0;
|
|
591
|
+
const { TimeoutError, GitHubApiError } = requireErrors();
|
|
592
|
+
const { withRetry } = requireRetry();
|
|
593
|
+
const GH_TIMEOUT_MS = 3e4;
|
|
594
|
+
function parseHttpStatus(stderr) {
|
|
595
|
+
if (!stderr) return null;
|
|
596
|
+
const match = stderr.match(/HTTP (\d{3})/);
|
|
597
|
+
return match ? parseInt(match[1], 10) : null;
|
|
598
|
+
}
|
|
181
599
|
function run(cmd) {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
600
|
+
try {
|
|
601
|
+
return execSync(cmd, {
|
|
602
|
+
encoding: "utf-8",
|
|
603
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
604
|
+
timeout: GH_TIMEOUT_MS
|
|
605
|
+
}).trim();
|
|
606
|
+
} catch (err) {
|
|
607
|
+
if (err.killed) {
|
|
608
|
+
throw new TimeoutError(
|
|
609
|
+
`gh command timed out after ${GH_TIMEOUT_MS / 1e3}s: ${cmd.slice(0, 80)}`,
|
|
610
|
+
{ timeoutMs: GH_TIMEOUT_MS, operation: cmd.slice(0, 120) }
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
const stderr = (err.stderr || "").trim();
|
|
614
|
+
const httpStatus = parseHttpStatus(stderr);
|
|
615
|
+
const ghErr = new GitHubApiError(stderr || err.message, {
|
|
616
|
+
cause: err,
|
|
617
|
+
status: httpStatus
|
|
618
|
+
});
|
|
619
|
+
if (err.code) ghErr.code = err.code;
|
|
620
|
+
throw ghErr;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
async function runWithRetry(cmd) {
|
|
624
|
+
return withRetry(async () => run(cmd));
|
|
186
625
|
}
|
|
187
|
-
function getRepo() {
|
|
188
|
-
return
|
|
626
|
+
async function getRepo() {
|
|
627
|
+
return runWithRetry("gh repo view --json nameWithOwner -q .nameWithOwner");
|
|
189
628
|
}
|
|
190
|
-
function getIssue(number) {
|
|
191
|
-
const raw =
|
|
629
|
+
async function getIssue(number) {
|
|
630
|
+
const raw = await runWithRetry(
|
|
192
631
|
`gh issue view ${number} --json number,title,state,labels,milestone,assignees,body`
|
|
193
632
|
);
|
|
194
633
|
return JSON.parse(raw);
|
|
195
634
|
}
|
|
196
|
-
function listIssues(filters) {
|
|
635
|
+
async function listIssues(filters) {
|
|
197
636
|
const f = filters || {};
|
|
198
637
|
let cmd = "gh issue list --json number,title,state,labels,milestone,assignees,createdAt,url,body,comments";
|
|
199
638
|
if (f.label) cmd += ` --label ${JSON.stringify(f.label)}`;
|
|
@@ -201,16 +640,16 @@ function requireGithub () {
|
|
|
201
640
|
if (f.assignee && f.assignee !== "all") cmd += ` --assignee ${JSON.stringify(f.assignee)}`;
|
|
202
641
|
if (f.state) cmd += ` --state ${f.state}`;
|
|
203
642
|
if (f.limit) cmd += ` --limit ${parseInt(f.limit, 10)}`;
|
|
204
|
-
const raw =
|
|
643
|
+
const raw = await runWithRetry(cmd);
|
|
205
644
|
return JSON.parse(raw);
|
|
206
645
|
}
|
|
207
|
-
function getMilestone(number) {
|
|
208
|
-
const repo = getRepo();
|
|
209
|
-
const raw =
|
|
646
|
+
async function getMilestone(number) {
|
|
647
|
+
const repo = await getRepo();
|
|
648
|
+
const raw = await runWithRetry(`gh api repos/${repo}/milestones/${number}`);
|
|
210
649
|
return JSON.parse(raw);
|
|
211
650
|
}
|
|
212
|
-
function getRateLimit() {
|
|
213
|
-
const raw =
|
|
651
|
+
async function getRateLimit() {
|
|
652
|
+
const raw = await runWithRetry("gh api rate_limit");
|
|
214
653
|
const data = JSON.parse(raw);
|
|
215
654
|
const core = data.resources.core;
|
|
216
655
|
return {
|
|
@@ -219,21 +658,21 @@ function requireGithub () {
|
|
|
219
658
|
reset: core.reset
|
|
220
659
|
};
|
|
221
660
|
}
|
|
222
|
-
function closeMilestone(repo, number) {
|
|
223
|
-
const raw =
|
|
661
|
+
async function closeMilestone(repo, number) {
|
|
662
|
+
const raw = await runWithRetry(
|
|
224
663
|
`gh api repos/${repo}/milestones/${number} --method PATCH -f state=closed`
|
|
225
664
|
);
|
|
226
665
|
return JSON.parse(raw);
|
|
227
666
|
}
|
|
228
|
-
function createRelease(repo, tag, title, opts) {
|
|
667
|
+
async function createRelease(repo, tag, title, opts) {
|
|
229
668
|
const o = opts || {};
|
|
230
669
|
let cmd = `gh release create ${JSON.stringify(tag)} --repo ${JSON.stringify(repo)} --title ${JSON.stringify(title)}`;
|
|
231
670
|
if (o.notes) cmd += ` --notes ${JSON.stringify(o.notes)}`;
|
|
232
671
|
if (o.draft) cmd += " --draft";
|
|
233
672
|
if (o.prerelease) cmd += " --prerelease";
|
|
234
|
-
return
|
|
673
|
+
return runWithRetry(cmd);
|
|
235
674
|
}
|
|
236
|
-
function getProjectNodeId(owner, projectNumber) {
|
|
675
|
+
async function getProjectNodeId(owner, projectNumber) {
|
|
237
676
|
const userQuery = `'query($login: String!, $number: Int!) { user(login: $login) { projectV2(number: $number) { id } } }'`;
|
|
238
677
|
const orgQuery = `'query($login: String!, $number: Int!) { organization(login: $login) { projectV2(number: $number) { id } } }'`;
|
|
239
678
|
try {
|
|
@@ -252,7 +691,7 @@ function requireGithub () {
|
|
|
252
691
|
}
|
|
253
692
|
return null;
|
|
254
693
|
}
|
|
255
|
-
function findExistingBoard(owner, titlePattern) {
|
|
694
|
+
async function findExistingBoard(owner, titlePattern) {
|
|
256
695
|
const pattern = titlePattern.toLowerCase();
|
|
257
696
|
try {
|
|
258
697
|
const raw = run(
|
|
@@ -274,7 +713,7 @@ function requireGithub () {
|
|
|
274
713
|
}
|
|
275
714
|
return null;
|
|
276
715
|
}
|
|
277
|
-
function getProjectFields(owner, projectNumber) {
|
|
716
|
+
async function getProjectFields(owner, projectNumber) {
|
|
278
717
|
const query = `'query($login: String!, $number: Int!) { user(login: $login) { projectV2(number: $number) { fields(first: 20) { nodes { ... on ProjectV2SingleSelectField { id name options { id name } } ... on ProjectV2Field { id name dataType } } } } } }'`;
|
|
279
718
|
const orgQuery = `'query($login: String!, $number: Int!) { organization(login: $login) { projectV2(number: $number) { fields(first: 20) { nodes { ... on ProjectV2SingleSelectField { id name options { id name } } ... on ProjectV2Field { id name dataType } } } } } }'`;
|
|
280
719
|
let raw;
|
|
@@ -352,19 +791,19 @@ function requireGithub () {
|
|
|
352
791
|
}
|
|
353
792
|
return Object.keys(fields).length > 0 ? fields : null;
|
|
354
793
|
}
|
|
355
|
-
function createProject(owner, title) {
|
|
356
|
-
const raw =
|
|
794
|
+
async function createProject(owner, title) {
|
|
795
|
+
const raw = await runWithRetry(
|
|
357
796
|
`gh project create --owner ${JSON.stringify(owner)} --title ${JSON.stringify(title)} --format json`
|
|
358
797
|
);
|
|
359
798
|
const data = JSON.parse(raw);
|
|
360
799
|
return { number: data.number, url: data.url };
|
|
361
800
|
}
|
|
362
|
-
function addItemToProject(owner, projectNumber, issueUrl) {
|
|
363
|
-
return
|
|
801
|
+
async function addItemToProject(owner, projectNumber, issueUrl) {
|
|
802
|
+
return runWithRetry(
|
|
364
803
|
`gh project item-add ${projectNumber} --owner ${JSON.stringify(owner)} --url ${JSON.stringify(issueUrl)}`
|
|
365
804
|
);
|
|
366
805
|
}
|
|
367
|
-
function postMilestoneStartAnnouncement(opts) {
|
|
806
|
+
async function postMilestoneStartAnnouncement(opts) {
|
|
368
807
|
const {
|
|
369
808
|
repo,
|
|
370
809
|
milestoneName,
|
|
@@ -435,6 +874,8 @@ function requireGithub () {
|
|
|
435
874
|
return { posted: false, method: "none", url: null };
|
|
436
875
|
}
|
|
437
876
|
github = {
|
|
877
|
+
GH_TIMEOUT_MS,
|
|
878
|
+
run,
|
|
438
879
|
getRepo,
|
|
439
880
|
getIssue,
|
|
440
881
|
listIssues,
|
|
@@ -517,15 +958,17 @@ function requireOutput () {
|
|
|
517
958
|
return output;
|
|
518
959
|
}
|
|
519
960
|
|
|
520
|
-
var
|
|
521
|
-
var
|
|
961
|
+
var providerClaude;
|
|
962
|
+
var hasRequiredProviderClaude;
|
|
522
963
|
|
|
523
|
-
function
|
|
524
|
-
if (
|
|
525
|
-
|
|
526
|
-
const { execSync, spawn } = require$$0
|
|
964
|
+
function requireProviderClaude () {
|
|
965
|
+
if (hasRequiredProviderClaude) return providerClaude;
|
|
966
|
+
hasRequiredProviderClaude = 1;
|
|
967
|
+
const { execSync, spawn } = require$$0;
|
|
527
968
|
const path = require$$1;
|
|
528
|
-
const fs = require$$
|
|
969
|
+
const fs = require$$2;
|
|
970
|
+
const { ClaudeNotAvailableError, TimeoutError } = requireErrors();
|
|
971
|
+
const PROVIDER_ID = "claude";
|
|
529
972
|
function getCommandsDir() {
|
|
530
973
|
const dir = path.join(__dirname, "..", "commands");
|
|
531
974
|
if (!fs.existsSync(dir)) {
|
|
@@ -536,37 +979,51 @@ This may indicate a corrupted installation. Try reinstalling mgw.`
|
|
|
536
979
|
}
|
|
537
980
|
return dir;
|
|
538
981
|
}
|
|
539
|
-
function
|
|
982
|
+
function assertAvailable() {
|
|
540
983
|
try {
|
|
541
984
|
execSync("claude --version", {
|
|
542
985
|
encoding: "utf-8",
|
|
543
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
986
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
987
|
+
timeout: 1e4
|
|
544
988
|
});
|
|
545
989
|
} catch (err) {
|
|
546
|
-
if (err.
|
|
547
|
-
|
|
548
|
-
"
|
|
990
|
+
if (err.killed) {
|
|
991
|
+
throw new TimeoutError(
|
|
992
|
+
"claude --version timed out after 10s",
|
|
993
|
+
{ timeoutMs: 1e4, operation: "claude --version" }
|
|
549
994
|
);
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
|
|
995
|
+
}
|
|
996
|
+
if (err.code === "ENOENT") {
|
|
997
|
+
throw new ClaudeNotAvailableError(
|
|
998
|
+
"claude CLI is not installed.\n\nInstall it with:\n npm install -g @anthropic-ai/claude-code\n\nThen run:\n claude login",
|
|
999
|
+
{ reason: "not-installed" }
|
|
553
1000
|
);
|
|
554
1001
|
}
|
|
555
|
-
|
|
1002
|
+
throw new ClaudeNotAvailableError(
|
|
1003
|
+
"claude CLI check failed.\nEnsure claude is installed and on your PATH.",
|
|
1004
|
+
{ reason: "check-failed", cause: err }
|
|
1005
|
+
);
|
|
556
1006
|
}
|
|
557
1007
|
try {
|
|
558
1008
|
execSync("claude auth status", {
|
|
559
1009
|
encoding: "utf-8",
|
|
560
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
1010
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1011
|
+
timeout: 1e4
|
|
561
1012
|
});
|
|
562
|
-
} catch {
|
|
563
|
-
|
|
564
|
-
|
|
1013
|
+
} catch (err) {
|
|
1014
|
+
if (err.killed) {
|
|
1015
|
+
throw new TimeoutError(
|
|
1016
|
+
"claude auth status timed out after 10s",
|
|
1017
|
+
{ timeoutMs: 1e4, operation: "claude auth status" }
|
|
1018
|
+
);
|
|
1019
|
+
}
|
|
1020
|
+
throw new ClaudeNotAvailableError(
|
|
1021
|
+
"claude CLI is not authenticated.\n\nRun:\n claude login\n\nThen retry your command.",
|
|
1022
|
+
{ reason: "not-authenticated" }
|
|
565
1023
|
);
|
|
566
|
-
process.exit(1);
|
|
567
1024
|
}
|
|
568
1025
|
}
|
|
569
|
-
function
|
|
1026
|
+
function invoke(commandFile, userPrompt, opts) {
|
|
570
1027
|
const o = opts || {};
|
|
571
1028
|
const args = ["-p"];
|
|
572
1029
|
if (commandFile) {
|
|
@@ -587,6 +1044,10 @@ This may indicate a corrupted installation. Try reinstalling mgw.`
|
|
|
587
1044
|
const stdio = o.quiet ? ["pipe", "pipe", "pipe"] : ["inherit", "inherit", "inherit"];
|
|
588
1045
|
const child = spawn("claude", args, { stdio });
|
|
589
1046
|
let output = "";
|
|
1047
|
+
const sigintHandler = () => {
|
|
1048
|
+
child.kill("SIGINT");
|
|
1049
|
+
};
|
|
1050
|
+
process.on("SIGINT", sigintHandler);
|
|
590
1051
|
if (o.quiet) {
|
|
591
1052
|
child.stdout.on("data", (chunk) => {
|
|
592
1053
|
output += chunk.toString();
|
|
@@ -596,23 +1057,220 @@ This may indicate a corrupted installation. Try reinstalling mgw.`
|
|
|
596
1057
|
});
|
|
597
1058
|
}
|
|
598
1059
|
child.on("error", (err) => {
|
|
1060
|
+
process.removeListener("SIGINT", sigintHandler);
|
|
599
1061
|
if (err.code === "ENOENT") {
|
|
600
|
-
reject(new
|
|
1062
|
+
reject(new ClaudeNotAvailableError(
|
|
1063
|
+
"claude CLI not found. Install with: npm install -g @anthropic-ai/claude-code",
|
|
1064
|
+
{ reason: "not-installed" }
|
|
1065
|
+
));
|
|
601
1066
|
} else {
|
|
602
1067
|
reject(err);
|
|
603
1068
|
}
|
|
604
1069
|
});
|
|
605
1070
|
child.on("close", (code) => {
|
|
1071
|
+
process.removeListener("SIGINT", sigintHandler);
|
|
1072
|
+
resolve({ exitCode: code || 0, output });
|
|
1073
|
+
});
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
providerClaude = { PROVIDER_ID, assertAvailable, invoke, getCommandsDir };
|
|
1077
|
+
return providerClaude;
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
var providerGemini;
|
|
1081
|
+
var hasRequiredProviderGemini;
|
|
1082
|
+
|
|
1083
|
+
function requireProviderGemini () {
|
|
1084
|
+
if (hasRequiredProviderGemini) return providerGemini;
|
|
1085
|
+
hasRequiredProviderGemini = 1;
|
|
1086
|
+
const { execSync, spawn } = require$$0;
|
|
1087
|
+
const path = require$$1;
|
|
1088
|
+
const fs = require$$2;
|
|
1089
|
+
const os = require$$3;
|
|
1090
|
+
const PROVIDER_ID = "gemini";
|
|
1091
|
+
function getCommandsDir() {
|
|
1092
|
+
const dir = path.join(os.homedir(), ".gemini", "commands", "mgw");
|
|
1093
|
+
if (!fs.existsSync(dir)) {
|
|
1094
|
+
throw new Error(
|
|
1095
|
+
`Commands directory not found at: ${dir}
|
|
1096
|
+
Run: node bin/mgw-install.cjs --provider gemini
|
|
1097
|
+
(or reinstall the mgw package to trigger postinstall)`
|
|
1098
|
+
);
|
|
1099
|
+
}
|
|
1100
|
+
return dir;
|
|
1101
|
+
}
|
|
1102
|
+
function assertAvailable() {
|
|
1103
|
+
try {
|
|
1104
|
+
execSync("gemini --version", {
|
|
1105
|
+
encoding: "utf-8",
|
|
1106
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1107
|
+
});
|
|
1108
|
+
} catch (err) {
|
|
1109
|
+
if (err.code === "ENOENT") {
|
|
1110
|
+
console.error(
|
|
1111
|
+
"Error: gemini CLI is not installed.\n\nInstall it with:\n npm install -g @google/gemini-cli\n\nThen run:\n gemini auth"
|
|
1112
|
+
);
|
|
1113
|
+
} else {
|
|
1114
|
+
console.error(
|
|
1115
|
+
"Error: gemini CLI check failed.\nEnsure gemini is installed and on your PATH."
|
|
1116
|
+
);
|
|
1117
|
+
}
|
|
1118
|
+
process.exit(1);
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
function invoke(commandFile, userPrompt, opts) {
|
|
1122
|
+
const o = opts || {};
|
|
1123
|
+
if (o.dryRun) {
|
|
1124
|
+
const dryArgs = ["-p"];
|
|
1125
|
+
if (o.model) dryArgs.push("--model", o.model);
|
|
1126
|
+
dryArgs.push(commandFile ? "<system>" + commandFile + "</system> " + (userPrompt || "run") : userPrompt || "run");
|
|
1127
|
+
console.log("Would invoke: gemini " + dryArgs.join(" "));
|
|
1128
|
+
return Promise.resolve({ exitCode: 0, output: "" });
|
|
1129
|
+
}
|
|
1130
|
+
let effectivePrompt = userPrompt || "run";
|
|
1131
|
+
if (commandFile) {
|
|
1132
|
+
const fileContents = fs.readFileSync(commandFile, "utf-8");
|
|
1133
|
+
effectivePrompt = "<system>\n" + fileContents + "\n</system>\n\n" + effectivePrompt;
|
|
1134
|
+
}
|
|
1135
|
+
const args = ["-p"];
|
|
1136
|
+
if (o.model) {
|
|
1137
|
+
args.push("--model", o.model);
|
|
1138
|
+
}
|
|
1139
|
+
args.push(effectivePrompt);
|
|
1140
|
+
return new Promise((resolve, reject) => {
|
|
1141
|
+
const stdio = o.quiet ? ["pipe", "pipe", "pipe"] : ["inherit", "inherit", "inherit"];
|
|
1142
|
+
const child = spawn("gemini", args, { stdio });
|
|
1143
|
+
let output = "";
|
|
1144
|
+
if (o.quiet) {
|
|
1145
|
+
child.stdout.on("data", function(chunk) {
|
|
1146
|
+
output += chunk.toString();
|
|
1147
|
+
});
|
|
1148
|
+
child.stderr.on("data", function(chunk) {
|
|
1149
|
+
output += chunk.toString();
|
|
1150
|
+
});
|
|
1151
|
+
}
|
|
1152
|
+
child.on("error", function(err) {
|
|
1153
|
+
if (err.code === "ENOENT") {
|
|
1154
|
+
reject(new Error("gemini CLI not found. Install with: npm install -g @google/gemini-cli"));
|
|
1155
|
+
} else {
|
|
1156
|
+
reject(err);
|
|
1157
|
+
}
|
|
1158
|
+
});
|
|
1159
|
+
child.on("close", function(code) {
|
|
606
1160
|
resolve({ exitCode: code || 0, output });
|
|
607
1161
|
});
|
|
608
1162
|
});
|
|
609
1163
|
}
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
1164
|
+
providerGemini = { PROVIDER_ID, assertAvailable, invoke, getCommandsDir };
|
|
1165
|
+
return providerGemini;
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
var providerOpencode;
|
|
1169
|
+
var hasRequiredProviderOpencode;
|
|
1170
|
+
|
|
1171
|
+
function requireProviderOpencode () {
|
|
1172
|
+
if (hasRequiredProviderOpencode) return providerOpencode;
|
|
1173
|
+
hasRequiredProviderOpencode = 1;
|
|
1174
|
+
const { execSync, spawn } = require$$0;
|
|
1175
|
+
const path = require$$1;
|
|
1176
|
+
const fs = require$$2;
|
|
1177
|
+
const os = require$$3;
|
|
1178
|
+
const PROVIDER_ID = "opencode";
|
|
1179
|
+
function getCommandsDir() {
|
|
1180
|
+
const dir = path.join(os.homedir(), ".opencode", "commands", "mgw");
|
|
1181
|
+
if (!fs.existsSync(dir)) {
|
|
1182
|
+
throw new Error(
|
|
1183
|
+
"Commands directory not found at: " + dir + "\nRun: node bin/mgw-install.cjs --provider opencode\n(or reinstall the mgw package to trigger postinstall)"
|
|
1184
|
+
);
|
|
1185
|
+
}
|
|
1186
|
+
return dir;
|
|
1187
|
+
}
|
|
1188
|
+
function assertAvailable() {
|
|
1189
|
+
try {
|
|
1190
|
+
execSync("opencode --version", {
|
|
1191
|
+
encoding: "utf-8",
|
|
1192
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1193
|
+
});
|
|
1194
|
+
} catch (err) {
|
|
1195
|
+
if (err.code === "ENOENT") {
|
|
1196
|
+
console.error(
|
|
1197
|
+
"Error: opencode CLI is not installed.\n\nInstall it with:\n npm install -g opencode-ai\n\n(or see https://opencode.ai for installation instructions)"
|
|
1198
|
+
);
|
|
1199
|
+
} else {
|
|
1200
|
+
console.error(
|
|
1201
|
+
"Error: opencode CLI check failed.\nEnsure opencode is installed and on your PATH."
|
|
1202
|
+
);
|
|
1203
|
+
}
|
|
1204
|
+
process.exit(1);
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
function invoke(commandFile, userPrompt, opts) {
|
|
1208
|
+
const o = opts || {};
|
|
1209
|
+
const args = ["run"];
|
|
1210
|
+
if (commandFile) {
|
|
1211
|
+
args.push("--system-prompt", commandFile);
|
|
1212
|
+
}
|
|
1213
|
+
if (o.model) {
|
|
1214
|
+
args.push("--model", o.model);
|
|
1215
|
+
}
|
|
1216
|
+
args.push(userPrompt || "run");
|
|
1217
|
+
if (o.dryRun) {
|
|
1218
|
+
console.log("Would invoke: opencode " + args.join(" "));
|
|
1219
|
+
return Promise.resolve({ exitCode: 0, output: "" });
|
|
1220
|
+
}
|
|
1221
|
+
return new Promise(function(resolve, reject) {
|
|
1222
|
+
const stdio = o.quiet ? ["pipe", "pipe", "pipe"] : ["inherit", "inherit", "inherit"];
|
|
1223
|
+
const child = spawn("opencode", args, { stdio });
|
|
1224
|
+
let output = "";
|
|
1225
|
+
if (o.quiet) {
|
|
1226
|
+
child.stdout.on("data", function(chunk) {
|
|
1227
|
+
output += chunk.toString();
|
|
1228
|
+
});
|
|
1229
|
+
child.stderr.on("data", function(chunk) {
|
|
1230
|
+
output += chunk.toString();
|
|
1231
|
+
});
|
|
1232
|
+
}
|
|
1233
|
+
child.on("error", function(err) {
|
|
1234
|
+
if (err.code === "ENOENT") {
|
|
1235
|
+
reject(new Error("opencode CLI not found. See https://opencode.ai for installation instructions."));
|
|
1236
|
+
} else {
|
|
1237
|
+
reject(err);
|
|
1238
|
+
}
|
|
1239
|
+
});
|
|
1240
|
+
child.on("close", function(code) {
|
|
1241
|
+
resolve({ exitCode: code || 0, output });
|
|
1242
|
+
});
|
|
1243
|
+
});
|
|
1244
|
+
}
|
|
1245
|
+
providerOpencode = { PROVIDER_ID, assertAvailable, invoke, getCommandsDir };
|
|
1246
|
+
return providerOpencode;
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
var providerManager;
|
|
1250
|
+
var hasRequiredProviderManager;
|
|
1251
|
+
|
|
1252
|
+
function requireProviderManager () {
|
|
1253
|
+
if (hasRequiredProviderManager) return providerManager;
|
|
1254
|
+
hasRequiredProviderManager = 1;
|
|
1255
|
+
const registry = {
|
|
1256
|
+
claude: requireProviderClaude(),
|
|
1257
|
+
gemini: requireProviderGemini(),
|
|
1258
|
+
opencode: requireProviderOpencode()
|
|
614
1259
|
};
|
|
615
|
-
|
|
1260
|
+
function getProvider(providerId) {
|
|
1261
|
+
const id = providerId || "claude";
|
|
1262
|
+
const provider = registry[id];
|
|
1263
|
+
if (!provider) {
|
|
1264
|
+
const available = Object.keys(registry).join(", ");
|
|
1265
|
+
throw new Error(`Unknown provider: "${id}". Available: ${available}`);
|
|
1266
|
+
}
|
|
1267
|
+
return provider;
|
|
1268
|
+
}
|
|
1269
|
+
function listProviders() {
|
|
1270
|
+
return Object.keys(registry);
|
|
1271
|
+
}
|
|
1272
|
+
providerManager = { ProviderManager: { getProvider, listProviders } };
|
|
1273
|
+
return providerManager;
|
|
616
1274
|
}
|
|
617
1275
|
|
|
618
1276
|
var spinner;
|
|
@@ -716,6 +1374,160 @@ function requireSpinner () {
|
|
|
716
1374
|
return spinner;
|
|
717
1375
|
}
|
|
718
1376
|
|
|
1377
|
+
var logger;
|
|
1378
|
+
var hasRequiredLogger;
|
|
1379
|
+
|
|
1380
|
+
function requireLogger () {
|
|
1381
|
+
if (hasRequiredLogger) return logger;
|
|
1382
|
+
hasRequiredLogger = 1;
|
|
1383
|
+
const path = require$$1;
|
|
1384
|
+
const fs = require$$2;
|
|
1385
|
+
function getLogDir(repoRoot) {
|
|
1386
|
+
const root = repoRoot || process.cwd();
|
|
1387
|
+
const logDir = path.join(root, ".mgw", "logs");
|
|
1388
|
+
if (!fs.existsSync(logDir)) {
|
|
1389
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
1390
|
+
}
|
|
1391
|
+
return logDir;
|
|
1392
|
+
}
|
|
1393
|
+
function getLogFile(repoRoot) {
|
|
1394
|
+
const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
1395
|
+
return path.join(getLogDir(repoRoot), `${date}.jsonl`);
|
|
1396
|
+
}
|
|
1397
|
+
function writeLog(entry) {
|
|
1398
|
+
const { repoRoot, ...rest } = entry;
|
|
1399
|
+
const record = {
|
|
1400
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1401
|
+
...rest
|
|
1402
|
+
};
|
|
1403
|
+
try {
|
|
1404
|
+
const logFile = getLogFile(repoRoot);
|
|
1405
|
+
fs.appendFileSync(logFile, JSON.stringify(record) + "\n");
|
|
1406
|
+
} catch {
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
function startTimer(entry) {
|
|
1410
|
+
const start = Date.now();
|
|
1411
|
+
return {
|
|
1412
|
+
finish(status, errorMsg) {
|
|
1413
|
+
writeLog({
|
|
1414
|
+
...entry,
|
|
1415
|
+
duration_ms: Date.now() - start,
|
|
1416
|
+
status,
|
|
1417
|
+
error: errorMsg || void 0
|
|
1418
|
+
});
|
|
1419
|
+
}
|
|
1420
|
+
};
|
|
1421
|
+
}
|
|
1422
|
+
function readLogs(opts) {
|
|
1423
|
+
const o = opts || {};
|
|
1424
|
+
const logDir = getLogDir(o.repoRoot);
|
|
1425
|
+
if (!fs.existsSync(logDir)) return [];
|
|
1426
|
+
let sinceDate = null;
|
|
1427
|
+
if (o.since) {
|
|
1428
|
+
const relativeMatch = o.since.match(/^(\d+)d$/);
|
|
1429
|
+
if (relativeMatch) {
|
|
1430
|
+
sinceDate = /* @__PURE__ */ new Date();
|
|
1431
|
+
sinceDate.setDate(sinceDate.getDate() - parseInt(relativeMatch[1], 10));
|
|
1432
|
+
} else {
|
|
1433
|
+
sinceDate = new Date(o.since);
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
let files;
|
|
1437
|
+
try {
|
|
1438
|
+
files = fs.readdirSync(logDir).filter((f) => f.endsWith(".jsonl")).sort();
|
|
1439
|
+
} catch {
|
|
1440
|
+
return [];
|
|
1441
|
+
}
|
|
1442
|
+
if (sinceDate) {
|
|
1443
|
+
const sinceStr = sinceDate.toISOString().slice(0, 10);
|
|
1444
|
+
files = files.filter((f) => f.replace(".jsonl", "") >= sinceStr);
|
|
1445
|
+
}
|
|
1446
|
+
const entries = [];
|
|
1447
|
+
for (const file of files) {
|
|
1448
|
+
const filePath = path.join(logDir, file);
|
|
1449
|
+
let content;
|
|
1450
|
+
try {
|
|
1451
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
1452
|
+
} catch {
|
|
1453
|
+
continue;
|
|
1454
|
+
}
|
|
1455
|
+
for (const line of content.split("\n")) {
|
|
1456
|
+
if (!line.trim()) continue;
|
|
1457
|
+
let entry;
|
|
1458
|
+
try {
|
|
1459
|
+
entry = JSON.parse(line);
|
|
1460
|
+
} catch {
|
|
1461
|
+
continue;
|
|
1462
|
+
}
|
|
1463
|
+
if (o.issue && entry.issue !== o.issue) continue;
|
|
1464
|
+
if (o.command && entry.command !== o.command) continue;
|
|
1465
|
+
if (o.stage && entry.stage !== o.stage) continue;
|
|
1466
|
+
entries.push(entry);
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
entries.reverse();
|
|
1470
|
+
if (o.limit && entries.length > o.limit) {
|
|
1471
|
+
return entries.slice(0, o.limit);
|
|
1472
|
+
}
|
|
1473
|
+
return entries;
|
|
1474
|
+
}
|
|
1475
|
+
function aggregateMetrics(entries) {
|
|
1476
|
+
if (!entries || entries.length === 0) {
|
|
1477
|
+
return {
|
|
1478
|
+
total: 0,
|
|
1479
|
+
byStatus: {},
|
|
1480
|
+
byCommand: {},
|
|
1481
|
+
avgDuration: 0,
|
|
1482
|
+
failureRate: 0
|
|
1483
|
+
};
|
|
1484
|
+
}
|
|
1485
|
+
const byStatus = {};
|
|
1486
|
+
const byCommand = {};
|
|
1487
|
+
let totalDuration = 0;
|
|
1488
|
+
let durationCount = 0;
|
|
1489
|
+
let failures = 0;
|
|
1490
|
+
for (const e of entries) {
|
|
1491
|
+
byStatus[e.status] = (byStatus[e.status] || 0) + 1;
|
|
1492
|
+
if (e.status === "error") failures++;
|
|
1493
|
+
if (e.command) {
|
|
1494
|
+
if (!byCommand[e.command]) {
|
|
1495
|
+
byCommand[e.command] = { count: 0, errors: 0, totalDuration: 0 };
|
|
1496
|
+
}
|
|
1497
|
+
byCommand[e.command].count++;
|
|
1498
|
+
if (e.status === "error") byCommand[e.command].errors++;
|
|
1499
|
+
if (typeof e.duration_ms === "number") {
|
|
1500
|
+
byCommand[e.command].totalDuration += e.duration_ms;
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
if (typeof e.duration_ms === "number") {
|
|
1504
|
+
totalDuration += e.duration_ms;
|
|
1505
|
+
durationCount++;
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
for (const cmd of Object.keys(byCommand)) {
|
|
1509
|
+
const c = byCommand[cmd];
|
|
1510
|
+
c.avgDuration = c.count > 0 ? Math.round(c.totalDuration / c.count) : 0;
|
|
1511
|
+
}
|
|
1512
|
+
return {
|
|
1513
|
+
total: entries.length,
|
|
1514
|
+
byStatus,
|
|
1515
|
+
byCommand,
|
|
1516
|
+
avgDuration: durationCount > 0 ? Math.round(totalDuration / durationCount) : 0,
|
|
1517
|
+
failureRate: entries.length > 0 ? Math.round(failures / entries.length * 100) : 0
|
|
1518
|
+
};
|
|
1519
|
+
}
|
|
1520
|
+
logger = {
|
|
1521
|
+
getLogDir,
|
|
1522
|
+
getLogFile,
|
|
1523
|
+
writeLog,
|
|
1524
|
+
startTimer,
|
|
1525
|
+
readLogs,
|
|
1526
|
+
aggregateMetrics
|
|
1527
|
+
};
|
|
1528
|
+
return logger;
|
|
1529
|
+
}
|
|
1530
|
+
|
|
719
1531
|
var renderer;
|
|
720
1532
|
var hasRequiredRenderer;
|
|
721
1533
|
|
|
@@ -1189,7 +2001,7 @@ var hasRequiredKeyboard;
|
|
|
1189
2001
|
function requireKeyboard () {
|
|
1190
2002
|
if (hasRequiredKeyboard) return keyboard;
|
|
1191
2003
|
hasRequiredKeyboard = 1;
|
|
1192
|
-
const { EventEmitter } = require$$0$
|
|
2004
|
+
const { EventEmitter } = require$$0$1;
|
|
1193
2005
|
const DEFAULT_BINDINGS = {
|
|
1194
2006
|
// Scroll
|
|
1195
2007
|
"j": "scroll-down",
|
|
@@ -1545,7 +2357,7 @@ function requireGraceful () {
|
|
|
1545
2357
|
let ms;
|
|
1546
2358
|
try {
|
|
1547
2359
|
ms = Date.now() - new Date(dateStr).getTime();
|
|
1548
|
-
} catch (
|
|
2360
|
+
} catch (_e) {
|
|
1549
2361
|
return "-";
|
|
1550
2362
|
}
|
|
1551
2363
|
if (ms < 0) return "now";
|
|
@@ -1808,9 +2620,13 @@ function requireTui () {
|
|
|
1808
2620
|
}
|
|
1809
2621
|
|
|
1810
2622
|
exports.getDefaultExportFromCjs = getDefaultExportFromCjs;
|
|
1811
|
-
exports.
|
|
2623
|
+
exports.requireErrors = requireErrors;
|
|
1812
2624
|
exports.requireGithub = requireGithub;
|
|
2625
|
+
exports.requireLogger = requireLogger;
|
|
1813
2626
|
exports.requireOutput = requireOutput;
|
|
2627
|
+
exports.requireProviderClaude = requireProviderClaude;
|
|
2628
|
+
exports.requireProviderManager = requireProviderManager;
|
|
2629
|
+
exports.requireRetry = requireRetry;
|
|
1814
2630
|
exports.requireSpinner = requireSpinner;
|
|
1815
2631
|
exports.requireState = requireState;
|
|
1816
2632
|
exports.requireTui = requireTui;
|