tissues 0.5.2 โ†’ 0.6.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 CHANGED
@@ -1,8 +1,8 @@
1
- # tissues ๐Ÿ”–
1
+ # tissues ๐Ÿงป
2
2
 
3
- AI-enhanced GitHub issue creation with built-in safety guardrails.
3
+ AI Enhanced GitHub Issues: create consistent, high-quality GitHub Issues cut for agents to make reliable updates to your codebase through rich context add-ons for git history, related issues, project rules, coding standards, risk and complexity scores, auto-labeling, and templates.
4
4
 
5
- Create structured, deduplicated GitHub issues from the command line โ€” with circuit breakers and rate limiting to prevent runaway issue creation when used inside AI agent workflows.
5
+ Agent Safety Mechanics: automatic data recovery, configurable rate limits, loop detection with circuit breakers, auditable signatures.
6
6
 
7
7
  [![npm version](https://img.shields.io/npm/v/tissues)](https://www.npmjs.com/package/tissues)
8
8
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
@@ -32,13 +32,13 @@ Requires Node.js >= 18.
32
32
  # Authenticate (auto-detects gh CLI token)
33
33
  tissues auth
34
34
 
35
- # Set your active repo (optional โ€” saves typing --repo every time)
36
- tissues use calebogden/tissues
35
+ # Set your active repo and configure AI (interactive wizard)
36
+ tissues config
37
37
 
38
38
  # Create an issue interactively (title as positional argument โ€” no quotes needed)
39
39
  tissues create fix the login bug
40
40
 
41
- # Check safety status before running in CI
41
+ # Check auth, safety, and config status
42
42
  tissues status
43
43
  ```
44
44
 
@@ -54,16 +54,6 @@ Authenticate with GitHub. Auto-detects your `gh` CLI token if you're already log
54
54
  tissues auth
55
55
  ```
56
56
 
57
- ### `tissues use`
58
-
59
- Set the active repository context. Optional โ€” all commands also accept `--repo` directly. Once set, subsequent commands use this repo automatically unless overridden.
60
-
61
- ```bash
62
- tissues use calebogden/tissues # set by name
63
- tissues use # prompts: pick a repo from your GitHub account
64
- tissues use --repo calebogden/tissues # same, via flag
65
- ```
66
-
67
57
  ### `tissues create`
68
58
 
69
59
  Create a new GitHub issue. Runs dedup checks and safety gates before creating anything.
@@ -77,13 +67,15 @@ tissues create [title...] [options]
77
67
  | Flag | Description |
78
68
  |---|---|
79
69
  | `--repo <owner/name>` | Override active repo |
80
- | `--template <name>` | Template to use: `bug`, `feature`, `security`, `performance`, `refactor` |
81
- | `--title <title>` | Issue title (skips interactive prompt) |
70
+ | `--template <name>` | Template to use: `default`, `bug`, `feature`, or custom |
71
+ | `--title <title>` | Issue title (skips interactive prompt and triage step โ€” title used as-is) |
82
72
  | `--body <text>` | Issue body / description โ€” the actual content describing the issue (skips interactive prompt) |
83
73
  | `--instructions <text>` | AI enhancement instructions: guides how the AI writes the issue body (e.g. "keep it under 200 words", "use formal tone"). Does not appear in the created issue. Optional โ€” skips interactive prompt when provided. |
84
74
  | `--no-enhance` | Skip AI enhancement, use rendered template as-is |
75
+ | `--pipeline` | Force multi-step AI pipeline even if config disabled |
76
+ | `--no-pipeline` | Force single-shot AI enhancement even if pipeline enabled |
85
77
  | `--batch <file>` | Create multiple issues from a JSON file. Each item supports: `title`, `body`, `template`, `labels`, `agent`, `session` |
86
- | `--labels <labels>` | Comma-separated labels to apply |
78
+ | `--labels <labels>` | Comma-separated labels to apply. If a label doesn't exist on the repo, tissues will prompt to create it automatically. |
87
79
  | `--agent <name>` | Agent identifier for attribution (default: `human`) |
88
80
  | `--session <id>` | Session ID for attribution and idempotency |
89
81
  | `--force` | Skip dedup warnings (still blocks on exact matches) |
@@ -113,6 +105,23 @@ tissues create \
113
105
  --dry-run
114
106
  ```
115
107
 
108
+ ### `tissues drafts`
109
+
110
+ Manage issue drafts. tissues saves your issue to `.tissues/drafts/` before any safety or dedup checks โ€” the drafts command lets you view, flush, retry failed items, or clear the queue.
111
+
112
+ ```bash
113
+ tissues drafts # interactive management menu
114
+ tissues drafts flush # flush pending issues (non-interactive)
115
+ ```
116
+
117
+ ### `tissues templates`
118
+
119
+ Manage issue templates interactively. View, edit, or create templates. Editing a built-in template creates a user copy that overrides the default.
120
+
121
+ ```bash
122
+ tissues templates
123
+ ```
124
+
116
125
  ### `tissues list`
117
126
 
118
127
  Browse open issues for the active repo.
@@ -124,7 +133,7 @@ tissues list --repo owner/other-repo
124
133
 
125
134
  ### `tissues status`
126
135
 
127
- Show circuit breaker state and rate limit usage for the active repo.
136
+ Show auth, AI config, safety status, and draft count for the active repo.
128
137
 
129
138
  ```bash
130
139
  tissues status
@@ -132,17 +141,51 @@ tissues status --agent claude-opus-4-6
132
141
  tissues status --reset # force-reset circuit breaker to closed
133
142
  ```
134
143
 
135
- Output:
144
+ ---
145
+
146
+ ## Data Recovery
147
+
148
+ tissues never silently discards your input. Before any safety check or network call, your issue is written to `.tissues/drafts/`. If a rate limit, circuit breaker, or network failure blocks submission, your work is saved and `tissues drafts flush` sends it when you're ready.
136
149
 
150
+ ```bash
151
+ # See what's waiting
152
+ tissues status # shows "Drafts: N pending"
153
+
154
+ # Manage or flush
155
+ tissues drafts # interactive: view, flush, retry, clear
156
+ tissues drafts flush # send all pending (non-interactive)
157
+ ```
158
+
159
+ โ†’ See [Non-Destructive Data Policy](docs/data-policy.md) and [Your Data in tissues](docs/user-data.md) for full details.
160
+
161
+ ---
162
+
163
+ ## AI Enhancement
164
+
165
+ tissues enhances your issues using an 8-step AI pipeline. You type freeform text into "What's the issue" and the pipeline derives everything: the **triage** step extracts a concise title and structured description, followed by duplicate check, context extraction, scope analysis, complexity scoring, risk assessment, label suggestion, and body formatting. Each step feeds the next, producing structured issues with scores, auto-labels, and rich sections.
166
+
167
+ ```bash
168
+ # Interactive setup
169
+ tissues config
170
+
171
+ # Or set a key directly
172
+ tissues config set ai.keys.anthropic sk-ant-your-key
137
173
  ```
138
- Circuit Breaker: closed โœ“
139
- Rate Limit: 3/10 per hour (7 remaining)
140
- Burst: 1/5 in last 5 min
141
- Global: 3/30 per hour (27 remaining)
142
174
 
143
- Last issue created: 12 minutes ago
144
- Fingerprints stored: 47
145
175
  ```
176
+ โœ“ Input analyzed: Fix OAuth 401 error on Safari
177
+ โœ“ Duplicate check (none)
178
+ โœ“ Context gathered
179
+ โœ“ Scope analyzed (3 files)
180
+ โœ“ Complexity: 4/10
181
+ โœ“ Risk: 2/10
182
+ โœ“ Labels: bug, P2-medium
183
+ โœ“ Body formatted
184
+ ```
185
+
186
+ The pipeline is resilient โ€” failed steps are skipped, and the format step always runs with whatever data is available. Use `--no-pipeline` for single-shot enhancement, or `--no-enhance` to skip AI entirely.
187
+
188
+ โ†’ See [AI Enhancement docs](docs/features/ai.md) for pipeline configuration, providers, routing rules, model selection, and token budgets.
146
189
 
147
190
  ---
148
191
 
@@ -234,20 +277,34 @@ Configuration is loaded and merged from three sources in ascending priority orde
234
277
  }
235
278
  ```
236
279
 
237
- ### `tissues config`
280
+ AI keys go in user-level config only (`~/.config/tissues/config.json`) โ€” never commit them:
238
281
 
239
- Get or set persistent user-level config values using dot notation. Values are stored in `~/.config/tissues/config.json`.
282
+ ```json
283
+ {
284
+ "ai": {
285
+ "provider": "anthropic",
286
+ "keys": {
287
+ "anthropic": "sk-ant-...",
288
+ "openai": "sk-..."
289
+ }
290
+ }
291
+ }
292
+ ```
240
293
 
241
- ```bash
242
- # Set a value
243
- tissues config set safety.maxPerHour 5
244
- tissues config set attribution.defaultAgent my-agent
294
+ ### `tissues config`
245
295
 
246
- # Read a value
247
- tissues config get safety.maxPerHour
296
+ Interactive config wizard or direct KVP access. Values are stored in `~/.config/tissues/config.json`.
248
297
 
249
- # Show all resolved config
250
- tissues config list
298
+ ```bash
299
+ # Interactive wizard (main menu: repo, AI provider, keys, budgets, templates, safety, routing, backup)
300
+ tissues config
301
+
302
+ # Direct KVP access
303
+ tissues config ai.provider # get
304
+ tissues config ai.provider anthropic # set
305
+ tissues config set safety.maxPerHour 5 # set (subcommand form)
306
+ tissues config get safety.maxPerHour # get (subcommand form)
307
+ tissues config list # show all resolved config
251
308
  ```
252
309
 
253
310
  ### Hooks
@@ -271,9 +328,6 @@ TISSUES_ISSUE_URL # https://github.com/owner/repo/issues/142
271
328
  | `default` | Generic summary + details + additional context |
272
329
  | `bug` | Steps to reproduce, expected vs actual behavior, environment |
273
330
  | `feature` | Motivation, proposed solution, alternatives |
274
- | `security` | Severity, affected components, suggested fix |
275
- | `performance` | Current metric, target metric, affected area |
276
- | `refactor` | Motivation, scope, risk assessment |
277
331
 
278
332
  ### Custom Templates
279
333
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tissues",
3
- "version": "0.5.2",
3
+ "version": "0.6.0",
4
4
  "description": "AI-enhanced GitHub issue creation with built-in safety guardrails. Wraps gh CLI with circuit breakers, rate limiting, dedup, and templates.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -9,7 +9,7 @@
9
9
  "scripts": {
10
10
  "dev": "node bin/tissues.js",
11
11
  "watch": "node --watch bin/tissues.js --help",
12
- "test": "node --test src/**/*.test.js",
12
+ "test": "node --experimental-test-module-mocks --test 'src/**/*.test.js'",
13
13
  "audit": "node scripts/audit.js",
14
14
  "build": "node scripts/build.js",
15
15
  "dev:install": "node scripts/dev-install.js",
@@ -42,12 +42,11 @@
42
42
  "LICENSE"
43
43
  ],
44
44
  "engines": {
45
- "node": ">=18"
45
+ "node": ">=20.12"
46
46
  },
47
47
  "dependencies": {
48
48
  "@inquirer/prompts": "^7.0.0",
49
49
  "better-sqlite3": "^11.0.0",
50
- "chalk": "^5.3.0",
51
50
  "commander": "^12.0.0",
52
51
  "conf": "^13.0.0",
53
52
  "ora": "^8.0.0"
package/src/cli.js CHANGED
@@ -3,33 +3,35 @@ import { createRequire } from 'module'
3
3
  const { version } = createRequire(import.meta.url)('../package.json')
4
4
  import { authCommand } from './commands/auth.js'
5
5
  import { configCommand } from './commands/config.js'
6
- import { useCommand } from './commands/use.js'
7
- import { createCommand } from './commands/create.js'
6
+ import { createCommand, draftCommand } from './commands/create.js'
8
7
  import { listCommand } from './commands/list.js'
9
8
  import { statusCommand } from './commands/status.js'
9
+ import { templatesCommand } from './commands/templates.js'
10
+ import { draftsCommand } from './commands/drafts.js'
11
+ import { aiCommand } from './commands/ai.js'
10
12
  import { store } from './lib/config.js'
11
- import { requireGh, getAuthStatus } from './lib/gh.js'
12
- import chalk from 'chalk'
13
+ import { requireGh } from './lib/gh.js'
14
+ import { dim } from './lib/color.js'
13
15
 
14
16
  export const program = new Command()
15
17
 
16
18
  function authDescription() {
17
- const base = 'Authenticate with GitHub (via gh CLI)'
18
- try {
19
- const status = getAuthStatus()
20
- const active = status.accounts?.find(a => a.active)
21
- if (!active) return base
22
- const check = chalk.green('โœ”')
23
- return `${base} ${check} ${chalk.dim(active.login)}`
24
- } catch {
25
- return base
26
- }
19
+ return 'Authenticate with GitHub (via gh CLI)'
27
20
  }
28
21
 
22
+ const SKIP_REPO_BANNER = new Set([
23
+ 'auth', 'login', 'status', 'switch', 'logout',
24
+ 'config', 'drafts', 'publish', 'templates', 'ai',
25
+ ])
26
+
29
27
  program
30
28
  .name('tissues')
31
29
  .description('AI-enhanced GitHub issue creation from the command line.')
32
30
  .version(version)
31
+ .configureHelp({
32
+ subcommandTerm: (cmd) => cmd.name(),
33
+ })
34
+ .helpCommand('help', 'Show this help')
33
35
  .hook('preAction', (_thisCommand, actionCommand) => {
34
36
  const name = actionCommand.name()
35
37
 
@@ -39,11 +41,12 @@ program
39
41
  // Ensure gh is installed before any command
40
42
  requireGh()
41
43
 
42
- // Show active repo context on every command (except auth/open)
43
- if (name !== 'auth' && name !== 'use' && name !== 'login' && name !== 'status' && name !== 'switch' && name !== 'logout') {
44
+ // Show active repo context on relevant commands only
45
+ const parentName = actionCommand.parent?.name?.()
46
+ if (!SKIP_REPO_BANNER.has(name) && !SKIP_REPO_BANNER.has(parentName)) {
44
47
  const activeRepo = store.get('activeRepo')
45
48
  if (activeRepo) {
46
- console.log(chalk.dim(`Working in: ${activeRepo}\n`))
49
+ console.log(dim(`Working in: ${activeRepo}\n`))
47
50
  }
48
51
  }
49
52
  })
@@ -57,13 +60,12 @@ Pkg: https://www.npmjs.com/package/tissues
57
60
  `,
58
61
  )
59
62
 
60
- program.hook('preAction', () => {
61
- // Lazily resolve auth description only when a command actually runs
62
- authCommand.description(authDescription())
63
- })
64
63
  program.addCommand(authCommand)
65
64
  program.addCommand(configCommand)
66
- program.addCommand(useCommand)
67
65
  program.addCommand(createCommand)
66
+ program.addCommand(draftCommand)
68
67
  program.addCommand(listCommand)
69
68
  program.addCommand(statusCommand)
69
+ program.addCommand(templatesCommand)
70
+ program.addCommand(draftsCommand)
71
+ program.addCommand(aiCommand)
@@ -0,0 +1,266 @@
1
+ import { Command } from 'commander'
2
+ import { confirm } from '@inquirer/prompts'
3
+ import { store } from '../lib/config.js'
4
+ import { theme } from '../lib/theme.js'
5
+ import { loadConfig, findRepoRoot } from '../lib/defaults.js'
6
+ import { requireAuth, getAuthenticatedUser, listLabels, listRepos } from '../lib/gh.js'
7
+ import { listTemplates } from '../lib/templates.js'
8
+ import { checkDuplicate } from '../lib/dedup.js'
9
+ import { resolveRoute } from '../lib/ai/router.js'
10
+ import { checkAvailable } from '../lib/ai/index.js'
11
+ import { runCreate } from './create.js'
12
+ import { red, yellow, dim, bold, cyan } from '../lib/color.js'
13
+ import ora from 'ora'
14
+
15
+ // ---------------------------------------------------------------------------
16
+ // System prompt โ€” tells the AI how to build a `tissues create` invocation
17
+ // ---------------------------------------------------------------------------
18
+
19
+ function buildSystemPrompt(context) {
20
+ const lines = [
21
+ 'You translate natural language into a `tissues create` command.',
22
+ 'Return ONLY a JSON object with the fields to pass to the create command.',
23
+ '',
24
+ 'Available fields:',
25
+ ' "title" (required) โ€” concise issue title',
26
+ ' "body" (optional) โ€” issue body in markdown',
27
+ ' "labels" (optional) โ€” comma-separated label names',
28
+ ' "template" (optional) โ€” template key to use',
29
+ ' "repo" (optional) โ€” "owner/name", omit to use the active repo',
30
+ '',
31
+ 'Rules:',
32
+ '- Return ONLY valid JSON. No markdown fences, no explanation.',
33
+ '- Write a clear, actionable title.',
34
+ '- If the user gives enough detail, write a well-structured markdown body.',
35
+ '- Only include fields the user mentions or that you can clearly infer.',
36
+ '- Pick labels from the available labels when appropriate.',
37
+ '- Pick a template that fits the issue type (bug, feature, default).',
38
+ ]
39
+
40
+ if (context.activeRepo) {
41
+ lines.push(`\nActive repo: ${context.activeRepo}`)
42
+ }
43
+ if (context.user) {
44
+ lines.push(`Authenticated user: ${context.user}`)
45
+ }
46
+ if (context.repos?.length) {
47
+ lines.push(`Available repos: ${context.repos.join(', ')}`)
48
+ }
49
+ if (context.templates?.length) {
50
+ const keys = [...new Set(context.templates.map((t) => t.key))]
51
+ lines.push(`Available templates: ${keys.join(', ')}`)
52
+ }
53
+ if (context.labels?.length) {
54
+ lines.push(`Labels on active repo: ${context.labels.join(', ')}`)
55
+ }
56
+
57
+ return lines.join('\n')
58
+ }
59
+
60
+ // ---------------------------------------------------------------------------
61
+ // Gather context for the AI prompt
62
+ // ---------------------------------------------------------------------------
63
+
64
+ function gatherContext(opts) {
65
+ const activeRepo = opts.repo || store.get('activeRepo')
66
+ const repoRoot = findRepoRoot()
67
+ const config = loadConfig(repoRoot)
68
+
69
+ const context = { activeRepo, config, repoRoot }
70
+
71
+ try { context.user = getAuthenticatedUser() } catch { /* non-fatal */ }
72
+ try { context.templates = listTemplates(repoRoot) } catch { /* non-fatal */ }
73
+ try { context.repos = listRepos({ limit: 30 }) } catch { /* non-fatal */ }
74
+
75
+ if (activeRepo) {
76
+ try { context.labels = listLabels(activeRepo) } catch { /* non-fatal */ }
77
+ }
78
+
79
+ return context
80
+ }
81
+
82
+ // ---------------------------------------------------------------------------
83
+ // Parse AI response into runCreate opts
84
+ // ---------------------------------------------------------------------------
85
+
86
+ function parseCreateOpts(raw) {
87
+ let cleaned = raw.trim()
88
+ if (cleaned.startsWith('```')) {
89
+ cleaned = cleaned.replace(/^```(?:json)?\s*\n?/, '').replace(/\n?```\s*$/, '')
90
+ }
91
+
92
+ const parsed = JSON.parse(cleaned)
93
+ if (!parsed.title) throw new Error('AI response missing "title"')
94
+ return parsed
95
+ }
96
+
97
+ // ---------------------------------------------------------------------------
98
+ // Command
99
+ // ---------------------------------------------------------------------------
100
+
101
+ export const aiCommand = new Command('ai')
102
+ .description('Create an issue from a natural language prompt')
103
+ .argument('[prompt...]', 'Describe the issue you want to create')
104
+ .option('--dry-run', 'Preview the create command without executing')
105
+ .option('--yes, -y', 'Skip confirmation, run create immediately')
106
+ .option('--repo <repo>', 'Override active repo')
107
+ .option('--provider <name>', 'AI provider override')
108
+ .option('--model <name>', 'AI model override')
109
+ .action(async (promptWords, opts) => {
110
+ if (!promptWords || promptWords.length === 0) {
111
+ aiCommand.help()
112
+ return
113
+ }
114
+ const prompt = promptWords.join(' ')
115
+
116
+ requireAuth()
117
+
118
+ // Gather context (no spinner โ€” fast local calls)
119
+ const context = gatherContext(opts)
120
+ const { config } = context
121
+ const repo = opts.repo || context.activeRepo
122
+
123
+ const aiContext = { provider: opts.provider, model: opts.model }
124
+
125
+ // Early dedup check โ€” before any LLM call
126
+ if (repo) {
127
+ const dedupSpinner = ora('Checking for duplicates...').start()
128
+ try {
129
+ const dedupResult = await checkDuplicate(repo, { title: prompt, agent: 'ai' })
130
+ dedupSpinner.stop()
131
+
132
+ if (dedupResult.action === 'block') {
133
+ const blockReasons = dedupResult.results.filter((r) => r.action === 'block')
134
+ for (const r of blockReasons) {
135
+ console.error(r.reason)
136
+ if (r.existingIssue?.number) {
137
+ console.error(dim(` #${r.existingIssue.number}${r.existingIssue.title ? ` โ€” ${r.existingIssue.title}` : ''}`))
138
+ if (r.existingIssue.url) console.error(dim(` ${r.existingIssue.url}`))
139
+ }
140
+ }
141
+ console.error(red('Duplicate detected โ€” issue not created.'))
142
+ process.exit(1)
143
+ }
144
+
145
+ if (dedupResult.action === 'warn') {
146
+ console.warn(yellow('Similar issue(s) found:'))
147
+ for (const r of dedupResult.results.filter((r) => r.action === 'warn')) {
148
+ console.warn(r.reason)
149
+ if (r.existingIssue?.number) {
150
+ console.warn(dim(` #${r.existingIssue.number}${r.existingIssue.title ? ` โ€” ${r.existingIssue.title}` : ''}`))
151
+ if (r.existingIssue.url) console.warn(dim(` ${r.existingIssue.url}`))
152
+ }
153
+ }
154
+ console.warn()
155
+ const proceed = await confirm({ message: 'Continue anyway?', default: false, theme })
156
+ if (!proceed) {
157
+ console.log(dim('Aborted.'))
158
+ process.exit(0)
159
+ }
160
+ }
161
+ } catch {
162
+ dedupSpinner.stop()
163
+ // Non-fatal โ€” proceed without dedup
164
+ }
165
+ }
166
+
167
+ // If AI isn't configured at all, go straight to create with the prompt as title
168
+ if (!checkAvailable(config, aiContext)) {
169
+ console.warn(yellow('AI not configured โ€” using prompt as issue title.'))
170
+ console.warn(dim('Run `tissues config` to set up an AI provider.\n'))
171
+ return fallbackToCreate(prompt, opts)
172
+ }
173
+
174
+ // Ask AI to build the create options
175
+ const systemPrompt = buildSystemPrompt(context)
176
+ const messages = [
177
+ { role: 'system', content: systemPrompt },
178
+ { role: 'user', content: prompt },
179
+ ]
180
+
181
+ const { adapter, model } = resolveRoute(config, aiContext)
182
+ const spinner = ora('Building create command...').start()
183
+ let raw
184
+ try {
185
+ raw = await adapter.complete(messages, { model, maxTokens: 2048 })
186
+ spinner.stop()
187
+ } catch (err) {
188
+ spinner.fail('AI failed')
189
+ if (/\b401\b/.test(err.message)) {
190
+ console.warn(red(' API key is invalid or expired.'))
191
+ console.warn(dim(' Run ') + cyan('tissues config') + dim(' to update your API key.'))
192
+ }
193
+ return fallbackToCreate(prompt, opts)
194
+ }
195
+
196
+ let plan
197
+ try {
198
+ plan = parseCreateOpts(raw)
199
+ } catch {
200
+ return fallbackToCreate(prompt, opts)
201
+ }
202
+
203
+ // Show what create will receive
204
+ const planRepo = plan.repo || context.activeRepo
205
+ console.log()
206
+ console.log(bold(' tissues create'))
207
+ console.log(` ${dim('--title')} ${plan.title}`)
208
+ if (planRepo) console.log(` ${dim('--repo')} ${planRepo}`)
209
+ if (plan.template) console.log(` ${dim('--template')} ${plan.template}`)
210
+ if (plan.labels) console.log(` ${dim('--labels')} ${plan.labels}`)
211
+ if (plan.body) {
212
+ console.log(` ${dim('--body')}`)
213
+ const preview = plan.body.length > 400
214
+ ? plan.body.slice(0, 400) + dim('\n ...(truncated)')
215
+ : plan.body
216
+ for (const line of preview.split('\n')) {
217
+ console.log(` ${dim(line)}`)
218
+ }
219
+ }
220
+ console.log()
221
+
222
+ if (opts.dryRun) {
223
+ console.log(dim(' --dry-run: stopping here.\n'))
224
+ process.exit(0)
225
+ }
226
+
227
+ if (!opts.yes) {
228
+ const ok = await confirm({ message: 'Run this create?', default: true, theme })
229
+ if (!ok) {
230
+ console.log(dim('Aborted.'))
231
+ process.exit(0)
232
+ }
233
+ }
234
+
235
+ // Hand off to the same runCreate that `tissues create` uses
236
+ try {
237
+ const result = await runCreate({
238
+ repo: planRepo,
239
+ title: plan.title,
240
+ body: plan.body || undefined,
241
+ labels: plan.labels || undefined,
242
+ template: plan.template || undefined,
243
+ })
244
+ if (result === null) process.exit(0)
245
+ } catch (err) {
246
+ process.exit(err.isAbort ? 0 : 1)
247
+ }
248
+ })
249
+
250
+ // ---------------------------------------------------------------------------
251
+ // Fallback โ€” AI unavailable, run create with the prompt as the title
252
+ // ---------------------------------------------------------------------------
253
+
254
+ async function fallbackToCreate(prompt, opts) {
255
+ try {
256
+ const result = await runCreate({
257
+ repo: opts.repo || undefined,
258
+ _titleDefault: prompt,
259
+ dryRun: opts.dryRun || false,
260
+ enhance: true, // let create's own AI enhancement try (it handles failure gracefully)
261
+ })
262
+ if (result === null) process.exit(0)
263
+ } catch (err) {
264
+ process.exit(err.isAbort ? 0 : 1)
265
+ }
266
+ }
@@ -7,7 +7,7 @@ import {
7
7
  authSwitch,
8
8
  authLogout,
9
9
  } from '../lib/gh.js'
10
- import chalk from 'chalk'
10
+ import { yellow, bold, dim, cyan, green } from '../lib/color.js'
11
11
 
12
12
  export const authCommand = new Command('auth')
13
13
  .description('Authenticate with GitHub (via gh CLI)')
@@ -35,10 +35,10 @@ authCommand.addCommand(
35
35
  const { ok, missing } = checkScopes()
36
36
  if (!ok) {
37
37
  console.log()
38
- console.log(chalk.yellow('โš  Missing required scope: ') + chalk.bold(missing.join(', ')))
39
- console.log(chalk.dim(' Fix with: ') + chalk.cyan(`gh auth refresh -s ${missing.join(',')}`))
38
+ console.log(yellow('โš  Missing required scope: ') + bold(missing.join(', ')))
39
+ console.log(dim(' Fix with: ') + cyan(`gh auth refresh -s ${missing.join(',')}`))
40
40
  } else {
41
- console.log(chalk.green('\nโœ“ Token has required scopes for issue creation'))
41
+ console.log(green('\nโœ“ Token has required scopes for issue creation'))
42
42
  }
43
43
  }),
44
44
  )