tissues 0.6.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/cli.js +3 -1
- package/src/commands/ai.js +2 -0
- package/src/commands/config.js +143 -69
- package/src/commands/create.js +10 -3
- package/src/commands/enhancements.js +282 -0
- package/src/lib/ai/adapters/command.js +23 -13
- package/src/lib/ai/body-template.js +15 -0
- package/src/lib/ai/enhance.js +48 -11
- package/src/lib/ai/enhancement-adapter.js +109 -0
- package/src/lib/ai/index.js +2 -2
- package/src/lib/ai/pipeline.js +20 -2
- package/src/lib/ai/router.js +95 -7
- package/src/lib/ai/steps.js +23 -3
- package/src/lib/dedup.js +36 -30
- package/src/lib/defaults.js +17 -1
- package/src/lib/enhancements.js +436 -0
- package/src/lib/gh.js +16 -10
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enhancement definitions — first-class pipeline step descriptors.
|
|
3
|
+
*
|
|
4
|
+
* Three-tier priority (same as templates): repo > user > built-in.
|
|
5
|
+
* File format: YAML-ish frontmatter + system prompt body (.md files).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import fs from 'node:fs'
|
|
9
|
+
import path from 'node:path'
|
|
10
|
+
import os from 'node:os'
|
|
11
|
+
import { findRepoRoot, loadConfig } from './defaults.js'
|
|
12
|
+
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Built-in enhancements (extracted from steps.js prompts)
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
export const BUILT_IN_ENHANCEMENTS = {
|
|
18
|
+
triage: {
|
|
19
|
+
key: 'triage',
|
|
20
|
+
name: 'Input Analysis',
|
|
21
|
+
maxTokens: 1024,
|
|
22
|
+
mode: 'always',
|
|
23
|
+
format: 'json',
|
|
24
|
+
contextKey: 'triage',
|
|
25
|
+
order: 10,
|
|
26
|
+
requires: [],
|
|
27
|
+
isStructural: true,
|
|
28
|
+
provider: null,
|
|
29
|
+
prompt: [
|
|
30
|
+
'You extract a structured GitHub issue title and description from freeform user input.',
|
|
31
|
+
'Return a JSON object with:',
|
|
32
|
+
' title: string — concise issue title, ≤80 characters, imperative mood (e.g. "Fix login timeout on Safari")',
|
|
33
|
+
' description: string — structured description expanding on the input, in markdown',
|
|
34
|
+
'The title must capture the core intent. The description should organize and expand the raw input into clear context.',
|
|
35
|
+
'Return ONLY valid JSON.',
|
|
36
|
+
].join('\n'),
|
|
37
|
+
},
|
|
38
|
+
dedup: {
|
|
39
|
+
key: 'dedup',
|
|
40
|
+
name: 'Duplicate Check',
|
|
41
|
+
maxTokens: 1024,
|
|
42
|
+
mode: 'auto',
|
|
43
|
+
format: 'json',
|
|
44
|
+
contextKey: 'dedupScore',
|
|
45
|
+
order: 15,
|
|
46
|
+
requires: [],
|
|
47
|
+
isStructural: false,
|
|
48
|
+
provider: null,
|
|
49
|
+
prompt: [
|
|
50
|
+
'You are a duplicate detection system for GitHub issues.',
|
|
51
|
+
'Compare the new issue against existing open issues.',
|
|
52
|
+
'Return a JSON object with:',
|
|
53
|
+
' confidence: number 0-100 (how likely this is a duplicate)',
|
|
54
|
+
' level: "high" | "medium" | "low" | "none"',
|
|
55
|
+
' matches: array of { number, reason } for similar issues',
|
|
56
|
+
'Return ONLY valid JSON.',
|
|
57
|
+
].join('\n'),
|
|
58
|
+
},
|
|
59
|
+
context: {
|
|
60
|
+
key: 'context',
|
|
61
|
+
name: 'Context Extraction',
|
|
62
|
+
maxTokens: 1024,
|
|
63
|
+
mode: 'always',
|
|
64
|
+
format: 'json',
|
|
65
|
+
contextKey: 'structuredContext',
|
|
66
|
+
order: 20,
|
|
67
|
+
requires: [],
|
|
68
|
+
isStructural: false,
|
|
69
|
+
provider: null,
|
|
70
|
+
prompt: [
|
|
71
|
+
'You are an expert at understanding software issues.',
|
|
72
|
+
'Extract structured context from the issue description.',
|
|
73
|
+
'Return a JSON object with:',
|
|
74
|
+
' problem: string — the core problem in one sentence',
|
|
75
|
+
' files: string[] — file paths mentioned or implied',
|
|
76
|
+
' errors: string[] — error messages mentioned',
|
|
77
|
+
' sessionContext: string — any relevant session/environment context',
|
|
78
|
+
'Return ONLY valid JSON.',
|
|
79
|
+
].join('\n'),
|
|
80
|
+
},
|
|
81
|
+
scope: {
|
|
82
|
+
key: 'scope',
|
|
83
|
+
name: 'Scope Analysis',
|
|
84
|
+
maxTokens: 1024,
|
|
85
|
+
mode: 'auto',
|
|
86
|
+
format: 'json',
|
|
87
|
+
contextKey: 'scopeAnalysis',
|
|
88
|
+
order: 30,
|
|
89
|
+
requires: [],
|
|
90
|
+
isStructural: false,
|
|
91
|
+
provider: null,
|
|
92
|
+
prompt: [
|
|
93
|
+
'You are a software scope analyzer.',
|
|
94
|
+
'Given an issue description and context, identify the files and areas affected.',
|
|
95
|
+
'Return a JSON object with:',
|
|
96
|
+
' files: array of { path: string, purpose: string, deps: string[] }',
|
|
97
|
+
' affectedAreas: string[] — high-level areas (e.g. "auth", "UI", "database")',
|
|
98
|
+
'Return ONLY valid JSON.',
|
|
99
|
+
].join('\n'),
|
|
100
|
+
},
|
|
101
|
+
complexity: {
|
|
102
|
+
key: 'complexity',
|
|
103
|
+
name: 'Complexity Scoring',
|
|
104
|
+
maxTokens: 512,
|
|
105
|
+
mode: 'auto',
|
|
106
|
+
format: 'json',
|
|
107
|
+
contextKey: 'complexity',
|
|
108
|
+
order: 40,
|
|
109
|
+
requires: [],
|
|
110
|
+
isStructural: false,
|
|
111
|
+
provider: null,
|
|
112
|
+
prompt: [
|
|
113
|
+
'You assess implementation complexity for GitHub issues.',
|
|
114
|
+
'Return a JSON object with:',
|
|
115
|
+
' score: number 1-10 (1=trivial, 10=massive refactor)',
|
|
116
|
+
' rationale: string — one sentence explaining the score',
|
|
117
|
+
'Return ONLY valid JSON.',
|
|
118
|
+
].join('\n'),
|
|
119
|
+
},
|
|
120
|
+
risk: {
|
|
121
|
+
key: 'risk',
|
|
122
|
+
name: 'Risk Assessment',
|
|
123
|
+
maxTokens: 512,
|
|
124
|
+
mode: 'auto',
|
|
125
|
+
format: 'json',
|
|
126
|
+
contextKey: 'risk',
|
|
127
|
+
order: 50,
|
|
128
|
+
requires: [],
|
|
129
|
+
isStructural: false,
|
|
130
|
+
provider: null,
|
|
131
|
+
prompt: [
|
|
132
|
+
'You assess implementation risk for GitHub issues.',
|
|
133
|
+
'Consider: breaking changes, data loss potential, security implications, blast radius.',
|
|
134
|
+
'Return a JSON object with:',
|
|
135
|
+
' score: number 1-10 (1=no risk, 10=extremely risky)',
|
|
136
|
+
' rationale: string — one sentence explaining the score',
|
|
137
|
+
'Return ONLY valid JSON.',
|
|
138
|
+
].join('\n'),
|
|
139
|
+
},
|
|
140
|
+
labels: {
|
|
141
|
+
key: 'labels',
|
|
142
|
+
name: 'Label Suggestion',
|
|
143
|
+
maxTokens: 512,
|
|
144
|
+
mode: 'auto',
|
|
145
|
+
format: 'json',
|
|
146
|
+
contextKey: 'aiLabels',
|
|
147
|
+
order: 60,
|
|
148
|
+
requires: [],
|
|
149
|
+
isStructural: false,
|
|
150
|
+
provider: null,
|
|
151
|
+
prompt: [
|
|
152
|
+
'You suggest GitHub labels for issues.',
|
|
153
|
+
'Return a JSON object with:',
|
|
154
|
+
' labels: string[] — labels to apply (must be from the available list)',
|
|
155
|
+
' reasoning: string — brief explanation',
|
|
156
|
+
'Only suggest labels that genuinely fit. Return ONLY valid JSON.',
|
|
157
|
+
].join('\n'),
|
|
158
|
+
},
|
|
159
|
+
format: {
|
|
160
|
+
key: 'format',
|
|
161
|
+
name: 'Body Formatting',
|
|
162
|
+
maxTokens: 4096,
|
|
163
|
+
mode: 'always',
|
|
164
|
+
format: 'markdown',
|
|
165
|
+
contextKey: 'body',
|
|
166
|
+
order: 90,
|
|
167
|
+
requires: [],
|
|
168
|
+
isStructural: true,
|
|
169
|
+
provider: null,
|
|
170
|
+
prompt: [
|
|
171
|
+
'You are an expert at writing clear, well-structured GitHub issues.',
|
|
172
|
+
'Given an issue title, description, and analysis from prior steps,',
|
|
173
|
+
'write a complete issue body in markdown.',
|
|
174
|
+
].join('\n'),
|
|
175
|
+
},
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Return the list of built-in enhancement keys.
|
|
180
|
+
* @returns {string[]}
|
|
181
|
+
*/
|
|
182
|
+
export function builtInEnhancementKeys() {
|
|
183
|
+
return Object.keys(BUILT_IN_ENHANCEMENTS)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ---------------------------------------------------------------------------
|
|
187
|
+
// Frontmatter parser
|
|
188
|
+
// ---------------------------------------------------------------------------
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Parse an enhancement file: YAML-ish frontmatter + system prompt body.
|
|
192
|
+
*
|
|
193
|
+
* @param {string} content - raw file content
|
|
194
|
+
* @returns {{ meta: object, prompt: string }}
|
|
195
|
+
*/
|
|
196
|
+
export function parseEnhancementFile(content) {
|
|
197
|
+
const meta = {}
|
|
198
|
+
let prompt = content
|
|
199
|
+
|
|
200
|
+
const fmMatch = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/)
|
|
201
|
+
if (fmMatch) {
|
|
202
|
+
const frontmatter = fmMatch[1]
|
|
203
|
+
prompt = fmMatch[2].trim()
|
|
204
|
+
|
|
205
|
+
for (const line of frontmatter.split('\n')) {
|
|
206
|
+
const m = line.match(/^(\w+):\s*(.+)$/)
|
|
207
|
+
if (!m) continue
|
|
208
|
+
const [, key, rawVal] = m
|
|
209
|
+
let val = rawVal.trim()
|
|
210
|
+
|
|
211
|
+
// Parse arrays: ["a", "b"] or [a, b]
|
|
212
|
+
if (val.startsWith('[') && val.endsWith(']')) {
|
|
213
|
+
val = val.slice(1, -1).split(',').map((s) => s.trim().replace(/^["']|["']$/g, '')).filter(Boolean)
|
|
214
|
+
} else if (val === 'true') {
|
|
215
|
+
val = true
|
|
216
|
+
} else if (val === 'false') {
|
|
217
|
+
val = false
|
|
218
|
+
} else if (/^\d+$/.test(val)) {
|
|
219
|
+
val = Number(val)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
meta[key] = val
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return { meta, prompt }
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// ---------------------------------------------------------------------------
|
|
230
|
+
// Helpers
|
|
231
|
+
// ---------------------------------------------------------------------------
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Resolve the enhancement directory for a given source.
|
|
235
|
+
*
|
|
236
|
+
* @param {'repo' | 'user'} source
|
|
237
|
+
* @param {string|null} repoRoot
|
|
238
|
+
* @param {object} cfg - loaded config object
|
|
239
|
+
* @returns {string}
|
|
240
|
+
*/
|
|
241
|
+
export function resolveEnhancementDir(source, repoRoot, cfg) {
|
|
242
|
+
if (source === 'user') {
|
|
243
|
+
return path.join(os.homedir(), '.config', 'tissues', 'enhancements')
|
|
244
|
+
}
|
|
245
|
+
// repo source
|
|
246
|
+
const enhDir = cfg.enhancements?.dir ?? '.tissues/enhancements'
|
|
247
|
+
if (path.isAbsolute(enhDir)) return enhDir
|
|
248
|
+
if (repoRoot) return path.join(repoRoot, enhDir)
|
|
249
|
+
return path.resolve(enhDir)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Read all `.md` files from a directory and return enhancement descriptors.
|
|
254
|
+
*
|
|
255
|
+
* @param {string} dir
|
|
256
|
+
* @param {'repo' | 'user'} source
|
|
257
|
+
* @returns {Array<object>}
|
|
258
|
+
*/
|
|
259
|
+
function readEnhancementsFromDir(dir, source) {
|
|
260
|
+
try {
|
|
261
|
+
const entries = fs.readdirSync(dir)
|
|
262
|
+
return entries
|
|
263
|
+
.filter((f) => f.endsWith('.md'))
|
|
264
|
+
.map((f) => {
|
|
265
|
+
const key = path.basename(f, '.md')
|
|
266
|
+
const raw = fs.readFileSync(path.join(dir, f), 'utf8')
|
|
267
|
+
const { meta, prompt } = parseEnhancementFile(raw)
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
key,
|
|
271
|
+
name: meta.name || key.charAt(0).toUpperCase() + key.slice(1),
|
|
272
|
+
maxTokens: meta.maxTokens || 1024,
|
|
273
|
+
mode: meta.mode || 'auto',
|
|
274
|
+
format: meta.format || 'json',
|
|
275
|
+
contextKey: meta.contextKey || key,
|
|
276
|
+
order: meta.order != null ? meta.order : 50,
|
|
277
|
+
requires: Array.isArray(meta.requires) ? meta.requires : [],
|
|
278
|
+
isStructural: false,
|
|
279
|
+
provider: meta.provider || null,
|
|
280
|
+
prompt,
|
|
281
|
+
source,
|
|
282
|
+
}
|
|
283
|
+
})
|
|
284
|
+
} catch {
|
|
285
|
+
return []
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// ---------------------------------------------------------------------------
|
|
290
|
+
// Public API
|
|
291
|
+
// ---------------------------------------------------------------------------
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* List all available enhancements from all sources.
|
|
295
|
+
*
|
|
296
|
+
* Priority (highest → lowest): repo > user > built-in
|
|
297
|
+
*
|
|
298
|
+
* @param {string} [repoRoot] - path to repo root; auto-detected if omitted
|
|
299
|
+
* @returns {Array<{ key: string, name: string, source: string, mode: string, order: number }>}
|
|
300
|
+
*/
|
|
301
|
+
export function listEnhancements(repoRoot) {
|
|
302
|
+
const root = repoRoot ?? findRepoRoot()
|
|
303
|
+
const cfg = loadConfig(root)
|
|
304
|
+
|
|
305
|
+
// Built-in (lowest priority)
|
|
306
|
+
const builtIn = Object.entries(BUILT_IN_ENHANCEMENTS).map(([key, enh]) => ({
|
|
307
|
+
key,
|
|
308
|
+
name: enh.name,
|
|
309
|
+
source: 'built-in',
|
|
310
|
+
mode: enh.mode,
|
|
311
|
+
order: enh.order,
|
|
312
|
+
provider: enh.provider || null,
|
|
313
|
+
}))
|
|
314
|
+
|
|
315
|
+
// User enhancements
|
|
316
|
+
const userDir = resolveEnhancementDir('user', root, cfg)
|
|
317
|
+
const userEnhancements = readEnhancementsFromDir(userDir, 'user').map(({ key, name, mode, order, provider }) => ({
|
|
318
|
+
key, name, source: 'user', mode, order, provider: provider || null,
|
|
319
|
+
}))
|
|
320
|
+
|
|
321
|
+
// Repo enhancements (highest priority)
|
|
322
|
+
const repoDir = resolveEnhancementDir('repo', root, cfg)
|
|
323
|
+
const repoEnhancements = readEnhancementsFromDir(repoDir, 'repo').map(({ key, name, mode, order, provider }) => ({
|
|
324
|
+
key, name, source: 'repo', mode, order, provider: provider || null,
|
|
325
|
+
}))
|
|
326
|
+
|
|
327
|
+
return [...repoEnhancements, ...userEnhancements, ...builtIn]
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Load a single enhancement by key, applying source priority:
|
|
332
|
+
* repo > user > built-in
|
|
333
|
+
*
|
|
334
|
+
* @param {string} name - enhancement key
|
|
335
|
+
* @param {string} [repoRoot]
|
|
336
|
+
* @returns {object} full enhancement descriptor with prompt
|
|
337
|
+
* @throws {Error} if enhancement not found
|
|
338
|
+
*/
|
|
339
|
+
export function loadEnhancement(name, repoRoot) {
|
|
340
|
+
const root = repoRoot ?? findRepoRoot()
|
|
341
|
+
const cfg = loadConfig(root)
|
|
342
|
+
|
|
343
|
+
// 1. Repo
|
|
344
|
+
const repoDir = resolveEnhancementDir('repo', root, cfg)
|
|
345
|
+
const repoFile = path.join(repoDir, `${name}.md`)
|
|
346
|
+
if (fs.existsSync(repoFile)) {
|
|
347
|
+
const raw = fs.readFileSync(repoFile, 'utf8')
|
|
348
|
+
return buildDescriptor(name, raw, 'repo')
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// 2. User
|
|
352
|
+
const userDir = resolveEnhancementDir('user', root, cfg)
|
|
353
|
+
const userFile = path.join(userDir, `${name}.md`)
|
|
354
|
+
if (fs.existsSync(userFile)) {
|
|
355
|
+
const raw = fs.readFileSync(userFile, 'utf8')
|
|
356
|
+
return buildDescriptor(name, raw, 'user')
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// 3. Built-in
|
|
360
|
+
const builtIn = BUILT_IN_ENHANCEMENTS[name]
|
|
361
|
+
if (builtIn) {
|
|
362
|
+
return { ...builtIn, source: 'built-in' }
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
throw new Error(
|
|
366
|
+
`Enhancement "${name}" not found. ` +
|
|
367
|
+
`Available: ${Object.keys(BUILT_IN_ENHANCEMENTS).join(', ')}`,
|
|
368
|
+
)
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Load all enhancements, merged by priority and sorted by order.
|
|
373
|
+
*
|
|
374
|
+
* @param {string} [repoRoot]
|
|
375
|
+
* @returns {object[]} sorted array of enhancement descriptors
|
|
376
|
+
*/
|
|
377
|
+
export function loadAllEnhancements(repoRoot) {
|
|
378
|
+
const root = repoRoot ?? findRepoRoot()
|
|
379
|
+
const cfg = loadConfig(root)
|
|
380
|
+
|
|
381
|
+
// Start with built-ins
|
|
382
|
+
const merged = new Map()
|
|
383
|
+
for (const [key, enh] of Object.entries(BUILT_IN_ENHANCEMENTS)) {
|
|
384
|
+
merged.set(key, { ...enh, source: 'built-in' })
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// User overrides
|
|
388
|
+
const userDir = resolveEnhancementDir('user', root, cfg)
|
|
389
|
+
for (const enh of readEnhancementsFromDir(userDir, 'user')) {
|
|
390
|
+
if (merged.has(enh.key)) {
|
|
391
|
+
// Override built-in but preserve isStructural
|
|
392
|
+
const builtIn = merged.get(enh.key)
|
|
393
|
+
merged.set(enh.key, { ...builtIn, ...enh, isStructural: builtIn.isStructural })
|
|
394
|
+
} else {
|
|
395
|
+
merged.set(enh.key, enh)
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Repo overrides (highest priority)
|
|
400
|
+
const repoDir = resolveEnhancementDir('repo', root, cfg)
|
|
401
|
+
for (const enh of readEnhancementsFromDir(repoDir, 'repo')) {
|
|
402
|
+
if (merged.has(enh.key)) {
|
|
403
|
+
const existing = merged.get(enh.key)
|
|
404
|
+
merged.set(enh.key, { ...existing, ...enh, isStructural: existing.isStructural || false })
|
|
405
|
+
} else {
|
|
406
|
+
merged.set(enh.key, enh)
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Sort by order
|
|
411
|
+
return [...merged.values()].sort((a, b) => a.order - b.order)
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// ---------------------------------------------------------------------------
|
|
415
|
+
// Internal helpers
|
|
416
|
+
// ---------------------------------------------------------------------------
|
|
417
|
+
|
|
418
|
+
function buildDescriptor(key, rawContent, source) {
|
|
419
|
+
const { meta, prompt } = parseEnhancementFile(rawContent)
|
|
420
|
+
const builtIn = BUILT_IN_ENHANCEMENTS[key]
|
|
421
|
+
|
|
422
|
+
return {
|
|
423
|
+
key,
|
|
424
|
+
name: meta.name || (builtIn ? builtIn.name : key.charAt(0).toUpperCase() + key.slice(1)),
|
|
425
|
+
maxTokens: meta.maxTokens || (builtIn ? builtIn.maxTokens : 1024),
|
|
426
|
+
mode: meta.mode || (builtIn ? builtIn.mode : 'auto'),
|
|
427
|
+
format: meta.format || (builtIn ? builtIn.format : 'json'),
|
|
428
|
+
contextKey: meta.contextKey || (builtIn ? builtIn.contextKey : key),
|
|
429
|
+
order: meta.order != null ? meta.order : (builtIn ? builtIn.order : 50),
|
|
430
|
+
requires: Array.isArray(meta.requires) ? meta.requires : (builtIn ? builtIn.requires : []),
|
|
431
|
+
isStructural: builtIn ? builtIn.isStructural : false,
|
|
432
|
+
provider: meta.provider || null,
|
|
433
|
+
prompt,
|
|
434
|
+
source,
|
|
435
|
+
}
|
|
436
|
+
}
|
package/src/lib/gh.js
CHANGED
|
@@ -321,16 +321,22 @@ export function addLabelsToIssue(repo, issueNumber, labels) {
|
|
|
321
321
|
*/
|
|
322
322
|
export function listIssues(repo, opts = {}) {
|
|
323
323
|
const limit = opts.limit ?? 100
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
'
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
324
|
+
let raw
|
|
325
|
+
try {
|
|
326
|
+
raw = execFileSync('gh', [
|
|
327
|
+
'issue', 'list',
|
|
328
|
+
'--repo', repo,
|
|
329
|
+
'--state', 'open',
|
|
330
|
+
'--limit', String(limit),
|
|
331
|
+
'--json', 'number,title,url,labels,createdAt',
|
|
332
|
+
], {
|
|
333
|
+
encoding: 'utf8',
|
|
334
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
335
|
+
}).trim()
|
|
336
|
+
} catch (err) {
|
|
337
|
+
const reason = (err.stderr || err.message || '').trim()
|
|
338
|
+
throw new Error(`gh issue list failed: ${reason}`)
|
|
339
|
+
}
|
|
334
340
|
|
|
335
341
|
if (!raw) return []
|
|
336
342
|
const issues = JSON.parse(raw)
|