tissues 0.5.2 → 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/README.md +94 -40
- package/package.json +3 -4
- package/src/cli.js +26 -22
- package/src/commands/ai.js +268 -0
- package/src/commands/auth.js +4 -4
- package/src/commands/config.js +1035 -12
- package/src/commands/create.js +523 -157
- package/src/commands/drafts.js +288 -0
- package/src/commands/enhancements.js +282 -0
- package/src/commands/list.js +7 -5
- package/src/commands/status.js +81 -19
- package/src/commands/templates.js +157 -0
- package/src/lib/ai/adapters/anthropic.js +52 -0
- package/src/lib/ai/adapters/base.js +45 -0
- package/src/lib/ai/adapters/command.js +68 -0
- package/src/lib/ai/adapters/gemini.js +56 -0
- package/src/lib/ai/adapters/ollama.js +60 -0
- package/src/lib/ai/adapters/openai-compat.js +51 -0
- package/src/lib/ai/adapters/openai.js +44 -0
- package/src/lib/ai/body-template.js +75 -0
- package/src/lib/ai/enhance.js +107 -0
- package/src/lib/ai/enhancement-adapter.js +109 -0
- package/src/lib/ai/index.js +122 -0
- package/src/lib/ai/pipeline.js +97 -0
- package/src/lib/ai/prompt.js +39 -0
- package/src/lib/ai/router.js +216 -0
- package/src/lib/ai/steps.js +492 -0
- package/src/lib/attribution.js +18 -179
- package/src/lib/clipboard.js +147 -0
- package/src/lib/color.js +9 -0
- package/src/lib/dedup.js +67 -32
- package/src/lib/defaults.js +54 -2
- package/src/lib/drafts.js +439 -0
- package/src/lib/enhancements.js +436 -0
- package/src/lib/gh.js +102 -21
- package/src/lib/repo-picker.js +2 -0
- package/src/lib/safety.js +1 -1
- package/src/lib/templates.js +8 -12
- package/src/lib/theme.js +9 -0
- package/src/commands/use.js +0 -19
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import { Command } from 'commander'
|
|
2
|
+
import { store } from '../lib/config.js'
|
|
3
|
+
import { loadConfig, findRepoRoot } from '../lib/defaults.js'
|
|
4
|
+
import { readDrafts, markComplete, markDuplicate, markFailed, countPending } from '../lib/drafts.js'
|
|
5
|
+
import { checkSafety, recordSuccess, recordFailure } from '../lib/safety.js'
|
|
6
|
+
import { checkDuplicate, recordCreation } from '../lib/dedup.js'
|
|
7
|
+
import { createIssue, verifyIssue } from '../lib/gh.js'
|
|
8
|
+
import { bold, dim, red } from '../lib/color.js'
|
|
9
|
+
import ora from 'ora'
|
|
10
|
+
import { select, confirm } from '@inquirer/prompts'
|
|
11
|
+
import { theme } from '../lib/theme.js'
|
|
12
|
+
|
|
13
|
+
function isCancelled(err) {
|
|
14
|
+
return err?.name === 'ExitPromptError' || err?.message?.includes('User force closed')
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function promptOrBack(fn) {
|
|
18
|
+
try { return await fn() } catch (err) { if (isCancelled(err)) return Symbol.for('back'); throw err }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function timeAgo(isoString) {
|
|
22
|
+
const ts = new Date(isoString)
|
|
23
|
+
const diffMs = Date.now() - ts.getTime()
|
|
24
|
+
if (diffMs < 0) return 'just now'
|
|
25
|
+
const seconds = Math.floor(diffMs / 1000)
|
|
26
|
+
if (seconds < 60) return `${seconds}s ago`
|
|
27
|
+
const minutes = Math.floor(diffMs / 60_000)
|
|
28
|
+
if (minutes < 60) return `${minutes}m ago`
|
|
29
|
+
const hours = Math.floor(diffMs / 3_600_000)
|
|
30
|
+
if (hours < 24) return `${hours}h ago`
|
|
31
|
+
const days = Math.floor(diffMs / 86_400_000)
|
|
32
|
+
return `${days}d ago`
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// Publish logic — creates GitHub issues from pending drafts
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
async function publishItems(repo, repoRoot, config, agent) {
|
|
40
|
+
const allItems = readDrafts(repoRoot)
|
|
41
|
+
const flushable = new Set(['pending', 'draft'])
|
|
42
|
+
const items = allItems.filter((item) => item.repo === repo && flushable.has(item.status))
|
|
43
|
+
|
|
44
|
+
if (items.length === 0) {
|
|
45
|
+
const total = countPending(repoRoot)
|
|
46
|
+
if (total > 0) {
|
|
47
|
+
console.log(dim(`No pending items for ${repo}.`))
|
|
48
|
+
console.log(dim(`(${total} item(s) belong to other repos or have failed)`))
|
|
49
|
+
} else {
|
|
50
|
+
console.log(dim('No drafts to publish.'))
|
|
51
|
+
}
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
console.log(bold(`Publishing ${items.length} issue${items.length === 1 ? '' : 's'}...\n`))
|
|
56
|
+
|
|
57
|
+
const flushed = []
|
|
58
|
+
const skipped = []
|
|
59
|
+
|
|
60
|
+
for (const item of items) {
|
|
61
|
+
const safetyResult = checkSafety(repo, agent, config.safety)
|
|
62
|
+
if (!safetyResult.allowed) {
|
|
63
|
+
const remaining = items.length - flushed.length - skipped.length
|
|
64
|
+
console.log()
|
|
65
|
+
console.log(dim(` ⏸ Sent ${flushed.length}/${items.length}. Rate limit reached — ${remaining} still in drafts.`))
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const dedupSpinner = ora(`Checking: "${item.title}"`).start()
|
|
70
|
+
let dedupResult
|
|
71
|
+
try {
|
|
72
|
+
dedupResult = await checkDuplicate(repo, { title: item.title, body: item.body, agent })
|
|
73
|
+
dedupSpinner.stop()
|
|
74
|
+
} catch (err) {
|
|
75
|
+
dedupSpinner.warn(`Dedup failed (skipping): ${err.message}`)
|
|
76
|
+
dedupResult = { action: 'allow', results: [] }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (dedupResult.action === 'block') {
|
|
80
|
+
recordFailure(repo, agent, config.safety)
|
|
81
|
+
console.log(dim(` ⊘ Skipping "${item.title}" — duplicate`))
|
|
82
|
+
const reason = dedupResult.results.filter((r) => r.action === 'block').map((r) => r.reason).join('; ')
|
|
83
|
+
markDuplicate(item.id, reason, repoRoot)
|
|
84
|
+
skipped.push(item)
|
|
85
|
+
continue
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const createSpinner = ora(`Creating: "${item.title}"`).start()
|
|
89
|
+
let issue
|
|
90
|
+
try {
|
|
91
|
+
issue = createIssue(repo, { title: item.title, body: item.body, labels: item.labels ?? [] })
|
|
92
|
+
createSpinner.succeed(`Created: ${issue.url}`)
|
|
93
|
+
} catch (err) {
|
|
94
|
+
createSpinner.fail(`Failed: "${item.title}" — ${err.message}`)
|
|
95
|
+
markFailed(item.id, err.message, repoRoot)
|
|
96
|
+
continue
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const verified = verifyIssue(repo, issue.number)
|
|
100
|
+
if (!verified) {
|
|
101
|
+
console.warn(dim(` #${issue.number} could not be verified — keeping in drafts`))
|
|
102
|
+
continue
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
markComplete(item.id, { issueNumber: issue.number, issueUrl: issue.url }, repoRoot)
|
|
106
|
+
try {
|
|
107
|
+
await recordCreation(repo, { title: item.title, body: item.body, issueNumber: issue.number, url: issue.url, agent })
|
|
108
|
+
} catch { /* non-fatal */ }
|
|
109
|
+
recordSuccess(repo, agent)
|
|
110
|
+
flushed.push({ title: item.title, number: issue.number, url: issue.url })
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
console.log()
|
|
114
|
+
if (flushed.length > 0) {
|
|
115
|
+
console.log(dim(` ✓ Published ${flushed.length} issue${flushed.length === 1 ? '' : 's'}`))
|
|
116
|
+
}
|
|
117
|
+
if (skipped.length > 0) {
|
|
118
|
+
console.log(dim(` ⊘ Skipped ${skipped.length} duplicate${skipped.length === 1 ? '' : 's'}`))
|
|
119
|
+
}
|
|
120
|
+
const stillPending = countPending(repoRoot)
|
|
121
|
+
if (stillPending === 0 && flushed.length > 0) {
|
|
122
|
+
console.log(dim(' No drafts remaining.'))
|
|
123
|
+
} else if (stillPending > 0) {
|
|
124
|
+
console.log(dim(` ${stillPending} item(s) still in drafts.`))
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
// Interactive drafts management
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
|
|
132
|
+
async function manageDrafts(repo, repoRoot) {
|
|
133
|
+
while (true) {
|
|
134
|
+
const allItems = readDrafts(repoRoot)
|
|
135
|
+
const repoItems = allItems.filter((item) => item.repo === repo)
|
|
136
|
+
const flushable = new Set(['pending', 'draft'])
|
|
137
|
+
const pending = repoItems.filter((item) => flushable.has(item.status))
|
|
138
|
+
const failed = repoItems.filter((item) => item.status === 'failed')
|
|
139
|
+
const duplicates = repoItems.filter((item) => item.status === 'duplicate')
|
|
140
|
+
const aborted = repoItems.filter((item) => item.status === 'aborted')
|
|
141
|
+
const otherRepoCount = allItems.length - repoItems.length
|
|
142
|
+
|
|
143
|
+
console.log()
|
|
144
|
+
if (repoItems.length === 0 && otherRepoCount === 0) {
|
|
145
|
+
console.log(dim(' No drafts.'))
|
|
146
|
+
return
|
|
147
|
+
} else if (repoItems.length === 0) {
|
|
148
|
+
console.log(dim(` No items for ${repo}.`))
|
|
149
|
+
if (otherRepoCount > 0) console.log(dim(` ${otherRepoCount} in other repos`))
|
|
150
|
+
} else {
|
|
151
|
+
if (pending.length > 0) console.log(dim(` ${pending.length} pending for ${repo}`))
|
|
152
|
+
if (failed.length > 0) console.log(dim(` ${failed.length} failed for ${repo}`))
|
|
153
|
+
if (duplicates.length > 0) console.log(dim(` ${duplicates.length} duplicate for ${repo}`))
|
|
154
|
+
if (aborted.length > 0) console.log(dim(` ${aborted.length} aborted for ${repo}`))
|
|
155
|
+
if (otherRepoCount > 0) console.log(dim(` ${otherRepoCount} in other repos`))
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const choices = []
|
|
159
|
+
if (pending.length > 0) choices.push({ name: `Publish ${pending.length} pending`, value: 'publish' })
|
|
160
|
+
if (repoItems.length > 0) choices.push({ name: 'View items', value: 'view' })
|
|
161
|
+
if (failed.length > 0) choices.push({ name: `Retry ${failed.length} failed`, value: 'retry' })
|
|
162
|
+
if (repoItems.length > 0) choices.push({ name: 'Delete all', value: 'delete-all' })
|
|
163
|
+
choices.push({ name: 'Done', value: 'done' })
|
|
164
|
+
|
|
165
|
+
const action = await promptOrBack(() => select({ message: 'Drafts', choices, theme }))
|
|
166
|
+
if (action === Symbol.for('back') || action === 'done') return
|
|
167
|
+
|
|
168
|
+
if (action === 'publish') {
|
|
169
|
+
const config = loadConfig(repoRoot)
|
|
170
|
+
const agent = config.attribution?.defaultAgent ?? 'human'
|
|
171
|
+
await publishItems(repo, repoRoot, config, agent)
|
|
172
|
+
continue
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (action === 'retry') {
|
|
176
|
+
const config = loadConfig(repoRoot)
|
|
177
|
+
const agent = config.attribution?.defaultAgent ?? 'human'
|
|
178
|
+
console.log(bold(`\nRetrying ${failed.length} failed item(s)...\n`))
|
|
179
|
+
for (const item of failed) {
|
|
180
|
+
const safetyResult = checkSafety(repo, agent, config.safety)
|
|
181
|
+
if (!safetyResult.allowed) {
|
|
182
|
+
console.log(dim(' ⏸ Rate limit — try again later.'))
|
|
183
|
+
break
|
|
184
|
+
}
|
|
185
|
+
const spinner = ora(`Creating: "${item.title}"`).start()
|
|
186
|
+
try {
|
|
187
|
+
const issue = createIssue(repo, { title: item.title, body: item.body, labels: item.labels ?? [] })
|
|
188
|
+
spinner.succeed(`Created: ${issue.url}`)
|
|
189
|
+
markComplete(item.id, { issueNumber: issue.number, issueUrl: issue.url }, repoRoot)
|
|
190
|
+
recordSuccess(repo, agent)
|
|
191
|
+
} catch (err) {
|
|
192
|
+
spinner.fail(`Failed again: ${err.message}`)
|
|
193
|
+
markFailed(item.id, err.message, repoRoot)
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
continue
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (action === 'view') {
|
|
200
|
+
const viewChoices = repoItems.map((item) => {
|
|
201
|
+
const statusMap = { failed: 'FAIL', duplicate: 'DUP ', aborted: dim('ABRT'), draft: 'DRFT' }
|
|
202
|
+
const status = statusMap[item.status] ?? 'PEND'
|
|
203
|
+
return {
|
|
204
|
+
name: `${status} ${item.title.slice(0, 40).padEnd(40)} ${dim(timeAgo(item.createdAt))}`,
|
|
205
|
+
value: item.id,
|
|
206
|
+
}
|
|
207
|
+
})
|
|
208
|
+
viewChoices.push({ name: dim('Back'), value: 'back' })
|
|
209
|
+
|
|
210
|
+
const itemId = await promptOrBack(() => select({ message: 'Draft items', choices: viewChoices, theme }))
|
|
211
|
+
if (itemId === Symbol.for('back') || itemId === 'back') continue
|
|
212
|
+
|
|
213
|
+
const item = repoItems.find((i) => i.id === itemId)
|
|
214
|
+
if (item) {
|
|
215
|
+
console.log(`\n ${bold(item.title)}`)
|
|
216
|
+
console.log(` Status: ${item.status} Created: ${item.createdAt}`)
|
|
217
|
+
if (item.error) console.log(` Error: ${item.error}`)
|
|
218
|
+
if (item.labels?.length) console.log(` Labels: ${item.labels.join(', ')}`)
|
|
219
|
+
console.log(dim(' ─'.repeat(20)))
|
|
220
|
+
console.log(dim(item.body?.slice(0, 300)))
|
|
221
|
+
if (item.body?.length > 300) console.log(dim('...'))
|
|
222
|
+
|
|
223
|
+
const itemAction = await promptOrBack(() =>
|
|
224
|
+
select({
|
|
225
|
+
message: 'Action',
|
|
226
|
+
choices: [
|
|
227
|
+
{ name: 'Delete this item', value: 'delete' },
|
|
228
|
+
{ name: dim('Back'), value: 'back' },
|
|
229
|
+
],
|
|
230
|
+
theme,
|
|
231
|
+
}),
|
|
232
|
+
)
|
|
233
|
+
if (itemAction === 'delete') {
|
|
234
|
+
markComplete(item.id, {}, repoRoot)
|
|
235
|
+
console.log(dim(` Removed "${item.title}"`))
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
continue
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (action === 'delete-all') {
|
|
242
|
+
const ok = await promptOrBack(() =>
|
|
243
|
+
confirm({ message: `Delete all ${repoItems.length} drafts for ${repo}?`, default: false, theme }),
|
|
244
|
+
)
|
|
245
|
+
if (ok === true) {
|
|
246
|
+
for (const item of repoItems) markComplete(item.id, {}, repoRoot)
|
|
247
|
+
console.log(dim(` Deleted ${repoItems.length} items`))
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ---------------------------------------------------------------------------
|
|
254
|
+
// Command
|
|
255
|
+
// ---------------------------------------------------------------------------
|
|
256
|
+
|
|
257
|
+
export const draftsCommand = new Command('drafts')
|
|
258
|
+
.description('Manage issue drafts (view, publish, retry, delete)')
|
|
259
|
+
.option('--repo <repo>', 'Repository override (owner/name)')
|
|
260
|
+
.action(async (opts) => {
|
|
261
|
+
let repo = opts.repo || store.get('activeRepo')
|
|
262
|
+
if (!repo) {
|
|
263
|
+
console.error(red('No active repo. Set one with: tissues config'))
|
|
264
|
+
process.exit(1)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const repoRoot = findRepoRoot()
|
|
268
|
+
await manageDrafts(repo, repoRoot)
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
// publish subcommand of drafts
|
|
272
|
+
draftsCommand.addCommand(
|
|
273
|
+
new Command('publish')
|
|
274
|
+
.description('Publish pending drafts as GitHub issues')
|
|
275
|
+
.option('--repo <repo>', 'Repository override')
|
|
276
|
+
.option('--agent <name>', 'Agent identifier (default: human)')
|
|
277
|
+
.action(async (opts) => {
|
|
278
|
+
let repo = opts.repo || store.get('activeRepo')
|
|
279
|
+
if (!repo) {
|
|
280
|
+
console.error(red('No active repo. Set one with: tissues config'))
|
|
281
|
+
process.exit(1)
|
|
282
|
+
}
|
|
283
|
+
const repoRoot = findRepoRoot()
|
|
284
|
+
const config = loadConfig(repoRoot)
|
|
285
|
+
const agent = opts.agent ?? config.attribution?.defaultAgent ?? 'human'
|
|
286
|
+
await publishItems(repo, repoRoot, config, agent)
|
|
287
|
+
}),
|
|
288
|
+
)
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { Command } from 'commander'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import os from 'node:os'
|
|
5
|
+
import { dim, red, green } from '../lib/color.js'
|
|
6
|
+
import { select, input, editor } from '@inquirer/prompts'
|
|
7
|
+
import {
|
|
8
|
+
listEnhancements,
|
|
9
|
+
loadEnhancement,
|
|
10
|
+
builtInEnhancementKeys,
|
|
11
|
+
BUILT_IN_ENHANCEMENTS,
|
|
12
|
+
parseEnhancementFile,
|
|
13
|
+
} from '../lib/enhancements.js'
|
|
14
|
+
import { findRepoRoot, loadConfig } from '../lib/defaults.js'
|
|
15
|
+
import { listProviders, listAllProviders } from '../lib/ai/index.js'
|
|
16
|
+
import { theme } from '../lib/theme.js'
|
|
17
|
+
|
|
18
|
+
const PROVIDER_LABELS = {
|
|
19
|
+
anthropic: 'Anthropic',
|
|
20
|
+
openai: 'OpenAI',
|
|
21
|
+
gemini: 'Gemini',
|
|
22
|
+
ollama: 'Ollama',
|
|
23
|
+
'openai-compat': 'OpenAI Custom',
|
|
24
|
+
command: 'Command',
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const builtInProviders = new Set(listProviders())
|
|
28
|
+
|
|
29
|
+
function formatProviderName(name) {
|
|
30
|
+
if (!name) return null
|
|
31
|
+
if (builtInProviders.has(name)) return PROVIDER_LABELS[name] || name
|
|
32
|
+
return `${name} ${dim('(custom)')}`
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function isCancelled(err) {
|
|
36
|
+
return err?.name === 'ExitPromptError' || err?.message?.includes('User force closed')
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function promptOrBack(fn) {
|
|
40
|
+
try { return await fn() } catch (err) { if (isCancelled(err)) return Symbol.for('back'); throw err }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function userEnhancementDir() {
|
|
44
|
+
return path.join(os.homedir(), '.config', 'tissues', 'enhancements')
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const enhancementsCommand = new Command('enhancements')
|
|
48
|
+
.description('Manage AI pipeline enhancements (view, edit, create)')
|
|
49
|
+
.action(async () => {
|
|
50
|
+
while (true) {
|
|
51
|
+
const repoRoot = findRepoRoot()
|
|
52
|
+
const enhancements = listEnhancements(repoRoot)
|
|
53
|
+
const builtInKeys = builtInEnhancementKeys()
|
|
54
|
+
|
|
55
|
+
// Deduplicate by key (higher-priority sources shadow lower ones)
|
|
56
|
+
const seen = new Set()
|
|
57
|
+
const choices = []
|
|
58
|
+
for (const enh of enhancements) {
|
|
59
|
+
if (!seen.has(enh.key)) {
|
|
60
|
+
seen.add(enh.key)
|
|
61
|
+
const provLabel = enh.provider ? formatProviderName(enh.provider) : 'default'
|
|
62
|
+
choices.push({
|
|
63
|
+
name: `${enh.key.padEnd(18)} ${dim(provLabel)}`,
|
|
64
|
+
value: enh.key,
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
choices.push({ name: green('Create New Enhancement'), value: '_create' })
|
|
69
|
+
choices.push({ name: dim('Done'), value: 'done' })
|
|
70
|
+
|
|
71
|
+
const chosen = await promptOrBack(() => select({ message: 'Enhancements', choices, theme }))
|
|
72
|
+
if (chosen === Symbol.for('back') || chosen === 'done') break
|
|
73
|
+
|
|
74
|
+
if (chosen === '_create') {
|
|
75
|
+
await createNewEnhancement(builtInKeys, seen)
|
|
76
|
+
continue
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// View / edit existing enhancement
|
|
80
|
+
await viewEnhancement(chosen, repoRoot, builtInKeys)
|
|
81
|
+
console.log()
|
|
82
|
+
}
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
async function createNewEnhancement(builtInKeys, existingKeys) {
|
|
86
|
+
const name = await promptOrBack(() => input({ message: 'Enhancement key (lowercase, no spaces)', theme }))
|
|
87
|
+
if (name === Symbol.for('back') || !name) return
|
|
88
|
+
|
|
89
|
+
const key = name.trim().toLowerCase().replace(/\s+/g, '-')
|
|
90
|
+
if (!key) return
|
|
91
|
+
|
|
92
|
+
// Block reusing exact built-in names
|
|
93
|
+
if (builtInKeys.includes(key)) {
|
|
94
|
+
console.log(red(` "${key}" is a built-in enhancement. Use edit to customize it instead.`))
|
|
95
|
+
return
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Block duplicates
|
|
99
|
+
if (existingKeys.has(key)) {
|
|
100
|
+
console.log(red(` Enhancement "${key}" already exists.`))
|
|
101
|
+
return
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const displayName = await promptOrBack(() =>
|
|
105
|
+
input({ message: 'Display name', default: key.charAt(0).toUpperCase() + key.slice(1), theme }),
|
|
106
|
+
)
|
|
107
|
+
if (displayName === Symbol.for('back')) return
|
|
108
|
+
|
|
109
|
+
const contextKey = await promptOrBack(() =>
|
|
110
|
+
input({ message: 'Context key (where result is stored)', default: key, theme }),
|
|
111
|
+
)
|
|
112
|
+
if (contextKey === Symbol.for('back')) return
|
|
113
|
+
|
|
114
|
+
const skeleton = [
|
|
115
|
+
'---',
|
|
116
|
+
`name: ${displayName}`,
|
|
117
|
+
'maxTokens: 1024',
|
|
118
|
+
'mode: auto',
|
|
119
|
+
'format: json',
|
|
120
|
+
`contextKey: ${contextKey}`,
|
|
121
|
+
'order: 50',
|
|
122
|
+
'requires: []',
|
|
123
|
+
'---',
|
|
124
|
+
`You are an expert at analyzing GitHub issues.`,
|
|
125
|
+
`Assess the ${key} aspects of the proposed change.`,
|
|
126
|
+
'Return a JSON object with your analysis.',
|
|
127
|
+
'Return ONLY valid JSON.',
|
|
128
|
+
].join('\n')
|
|
129
|
+
|
|
130
|
+
const body = await promptOrBack(() =>
|
|
131
|
+
editor({
|
|
132
|
+
message: 'Enhancement file (opens editor)',
|
|
133
|
+
default: skeleton,
|
|
134
|
+
theme,
|
|
135
|
+
}),
|
|
136
|
+
)
|
|
137
|
+
if (body === Symbol.for('back') || !body) return
|
|
138
|
+
|
|
139
|
+
const dir = userEnhancementDir()
|
|
140
|
+
fs.mkdirSync(dir, { recursive: true })
|
|
141
|
+
const filePath = path.join(dir, `${key}.md`)
|
|
142
|
+
fs.writeFileSync(filePath, body, 'utf8')
|
|
143
|
+
console.log(green(` ✔ Enhancement "${key}" created: ${filePath}`))
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function viewEnhancement(key, repoRoot, builtInKeys) {
|
|
147
|
+
let enh
|
|
148
|
+
try {
|
|
149
|
+
enh = loadEnhancement(key, repoRoot)
|
|
150
|
+
} catch (err) {
|
|
151
|
+
console.log(red(` ${err.message}`))
|
|
152
|
+
return
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
console.log(`\n ${enh.name} ${dim(`(${enh.source})`)}`)
|
|
156
|
+
console.log(dim(' ─'.repeat(20)))
|
|
157
|
+
console.log(dim(` Mode: ${enh.mode} Format: ${enh.format} Tokens: ${enh.maxTokens} Order: ${enh.order}`))
|
|
158
|
+
if (enh.provider) console.log(dim(` Provider: ${enh.provider}`))
|
|
159
|
+
if (enh.contextKey) console.log(dim(` Context key: ${enh.contextKey}`))
|
|
160
|
+
if (enh.requires?.length) console.log(dim(` Requires: ${enh.requires.join(', ')}`))
|
|
161
|
+
console.log()
|
|
162
|
+
|
|
163
|
+
// Show prompt preview (truncated)
|
|
164
|
+
const preview = enh.prompt.split('\n').slice(0, 8).join('\n')
|
|
165
|
+
console.log(dim(preview))
|
|
166
|
+
if (enh.prompt.split('\n').length > 8) console.log(dim(' ...'))
|
|
167
|
+
console.log()
|
|
168
|
+
|
|
169
|
+
const actionChoices = [
|
|
170
|
+
{ name: 'Edit', value: 'edit' },
|
|
171
|
+
{ name: `Provider ${formatProviderName(enh.provider) || dim('default')}`, value: 'provider' },
|
|
172
|
+
]
|
|
173
|
+
if (builtInKeys.includes(key) && enh.source === 'built-in') {
|
|
174
|
+
actionChoices[0] = { name: 'Customize (creates user copy)', value: 'edit' }
|
|
175
|
+
}
|
|
176
|
+
// Only allow deleting user enhancements
|
|
177
|
+
const userFile = path.join(userEnhancementDir(), `${key}.md`)
|
|
178
|
+
if (fs.existsSync(userFile)) {
|
|
179
|
+
actionChoices.push({ name: red('Delete user copy'), value: 'delete' })
|
|
180
|
+
}
|
|
181
|
+
actionChoices.push({ name: dim('Back'), value: 'back' })
|
|
182
|
+
|
|
183
|
+
const action = await promptOrBack(() => select({ message: enh.name, choices: actionChoices, theme }))
|
|
184
|
+
if (action === Symbol.for('back') || action === 'back') return
|
|
185
|
+
|
|
186
|
+
if (action === 'provider') {
|
|
187
|
+
const cfg = loadConfig()
|
|
188
|
+
const allProviders = listAllProviders(cfg)
|
|
189
|
+
const providerChoices = [
|
|
190
|
+
{ name: 'Default (use pipeline provider)', value: '_default' },
|
|
191
|
+
...allProviders.map((p) => ({
|
|
192
|
+
name: formatProviderName(p),
|
|
193
|
+
value: p,
|
|
194
|
+
})),
|
|
195
|
+
{ name: dim('Back'), value: 'back' },
|
|
196
|
+
]
|
|
197
|
+
|
|
198
|
+
const chosen = await promptOrBack(() =>
|
|
199
|
+
select({
|
|
200
|
+
message: `Provider for ${enh.name}`,
|
|
201
|
+
choices: providerChoices,
|
|
202
|
+
default: enh.provider || '_default',
|
|
203
|
+
theme,
|
|
204
|
+
}),
|
|
205
|
+
)
|
|
206
|
+
if (chosen === Symbol.for('back') || chosen === 'back') return
|
|
207
|
+
|
|
208
|
+
const newProvider = chosen === '_default' ? null : chosen
|
|
209
|
+
|
|
210
|
+
// Update the .md file frontmatter (creates user copy if built-in)
|
|
211
|
+
const updatedEnh = { ...enh, provider: newProvider }
|
|
212
|
+
const content = buildFileContent(updatedEnh)
|
|
213
|
+
const dir = userEnhancementDir()
|
|
214
|
+
fs.mkdirSync(dir, { recursive: true })
|
|
215
|
+
fs.writeFileSync(path.join(dir, `${key}.md`), content, 'utf8')
|
|
216
|
+
|
|
217
|
+
if (newProvider) {
|
|
218
|
+
console.log(green(` ✔ ${enh.name}: → ${newProvider}`))
|
|
219
|
+
} else {
|
|
220
|
+
console.log(green(` ✔ ${enh.name}: using default provider`))
|
|
221
|
+
}
|
|
222
|
+
if (enh.source === 'built-in') {
|
|
223
|
+
console.log(dim(` User copy created (overrides built-in)`))
|
|
224
|
+
}
|
|
225
|
+
return
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (action === 'delete') {
|
|
229
|
+
fs.unlinkSync(userFile)
|
|
230
|
+
console.log(green(` ✔ User copy of "${key}" deleted`))
|
|
231
|
+
if (builtInKeys.includes(key)) {
|
|
232
|
+
console.log(dim(` Built-in "${key}" will be used again.`))
|
|
233
|
+
}
|
|
234
|
+
return
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Edit — build the file content from the current enhancement
|
|
238
|
+
const currentContent = buildFileContent(enh)
|
|
239
|
+
|
|
240
|
+
const newBody = await promptOrBack(() =>
|
|
241
|
+
editor({
|
|
242
|
+
message: `Edit ${key}`,
|
|
243
|
+
default: currentContent,
|
|
244
|
+
theme,
|
|
245
|
+
}),
|
|
246
|
+
)
|
|
247
|
+
if (newBody === Symbol.for('back') || !newBody) return
|
|
248
|
+
|
|
249
|
+
// Save as user enhancement
|
|
250
|
+
const dir = userEnhancementDir()
|
|
251
|
+
fs.mkdirSync(dir, { recursive: true })
|
|
252
|
+
fs.writeFileSync(path.join(dir, `${key}.md`), newBody, 'utf8')
|
|
253
|
+
|
|
254
|
+
if (enh.source === 'built-in') {
|
|
255
|
+
console.log(green(` ✔ User copy of "${key}" created (overrides built-in)`))
|
|
256
|
+
} else {
|
|
257
|
+
console.log(green(` ✔ Enhancement "${key}" updated`))
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function buildFileContent(enh) {
|
|
262
|
+
const lines = [
|
|
263
|
+
'---',
|
|
264
|
+
`name: ${enh.name}`,
|
|
265
|
+
`maxTokens: ${enh.maxTokens}`,
|
|
266
|
+
`mode: ${enh.mode}`,
|
|
267
|
+
`format: ${enh.format}`,
|
|
268
|
+
`contextKey: ${enh.contextKey}`,
|
|
269
|
+
`order: ${enh.order}`,
|
|
270
|
+
]
|
|
271
|
+
if (enh.provider) {
|
|
272
|
+
lines.push(`provider: ${enh.provider}`)
|
|
273
|
+
}
|
|
274
|
+
if (enh.requires?.length) {
|
|
275
|
+
lines.push(`requires: [${enh.requires.map((r) => `"${r}"`).join(', ')}]`)
|
|
276
|
+
} else {
|
|
277
|
+
lines.push('requires: []')
|
|
278
|
+
}
|
|
279
|
+
lines.push('---')
|
|
280
|
+
lines.push(enh.prompt)
|
|
281
|
+
return lines.join('\n')
|
|
282
|
+
}
|
package/src/commands/list.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { Command } from 'commander'
|
|
2
2
|
import { search } from '@inquirer/prompts'
|
|
3
3
|
import { store } from '../lib/config.js'
|
|
4
|
+
import { theme } from '../lib/theme.js'
|
|
4
5
|
import { pickRepo } from '../lib/repo-picker.js'
|
|
5
6
|
import { requireAuth, listIssues } from '../lib/gh.js'
|
|
6
|
-
import
|
|
7
|
+
import { yellow, dim, cyan } from '../lib/color.js'
|
|
7
8
|
import ora from 'ora'
|
|
8
9
|
import { execSync } from 'child_process'
|
|
9
10
|
|
|
@@ -19,7 +20,7 @@ export const listCommand = new Command('list')
|
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
if (!repo) {
|
|
22
|
-
console.log(
|
|
23
|
+
console.log(yellow('No active repo. Pick one:\n'))
|
|
23
24
|
repo = await pickRepo()
|
|
24
25
|
console.log()
|
|
25
26
|
}
|
|
@@ -29,12 +30,12 @@ export const listCommand = new Command('list')
|
|
|
29
30
|
spinner.stop()
|
|
30
31
|
|
|
31
32
|
if (issues.length === 0) {
|
|
32
|
-
console.log(
|
|
33
|
+
console.log(dim('No open issues.'))
|
|
33
34
|
return
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
const selected = await search({
|
|
37
|
-
message: `Open issues in ${
|
|
38
|
+
message: `Open issues in ${cyan(repo)}`,
|
|
38
39
|
source: (input) => {
|
|
39
40
|
const term = (input || '').toLowerCase()
|
|
40
41
|
return issues
|
|
@@ -48,12 +49,13 @@ export const listCommand = new Command('list')
|
|
|
48
49
|
value: i.url,
|
|
49
50
|
}))
|
|
50
51
|
},
|
|
52
|
+
theme,
|
|
51
53
|
})
|
|
52
54
|
|
|
53
55
|
// Open in browser
|
|
54
56
|
try {
|
|
55
57
|
execSync(`open "${selected}"`)
|
|
56
58
|
} catch {
|
|
57
|
-
console.log(`\nOpen: ${
|
|
59
|
+
console.log(`\nOpen: ${cyan(selected)}`)
|
|
58
60
|
}
|
|
59
61
|
})
|