infra-kit 0.1.79 → 0.1.81

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.
@@ -1,300 +1,82 @@
1
1
  import { z } from 'zod'
2
- import { $ } from 'zx'
3
2
 
4
- import { getCurrentWorktrees, getProjectRoot } from 'src/lib/git-utils'
3
+ import { getReleasePRsWithInfo } from 'src/integrations/gh'
4
+ import { getCurrentWorktrees } from 'src/lib/git-utils'
5
5
  import { logger } from 'src/lib/logger'
6
+ import { detectReleaseType, formatVersionLabel, getJiraDescriptions } from 'src/lib/release-utils'
7
+ import type { ReleaseType } from 'src/lib/release-utils'
6
8
  import type { ToolsExecutionResult } from 'src/types'
7
9
 
8
10
  interface WorktreeInfo {
9
- branch: string
10
- path: string
11
- commit: string
12
- isCurrent: boolean
13
- type: 'release' | 'feature'
14
- status: string
15
- lastCommitMessage: string
16
- aheadBehind: string
11
+ version: string
12
+ type: ReleaseType
13
+ description: string | null
17
14
  }
18
15
 
19
16
  /**
20
- * List all (features and releases) git worktrees with detailed information
17
+ * List all release git worktrees with version, type, and Jira description
21
18
  */
22
19
  export const worktreesList = async (): Promise<ToolsExecutionResult> => {
23
- try {
24
- const [releaseWorktrees, featureWorktrees] = await Promise.all([
25
- getCurrentWorktrees('release'),
26
- getCurrentWorktrees('feature'),
27
- ])
20
+ const currentWorktrees = await getCurrentWorktrees('release')
28
21
 
29
- const projectRoot = await getProjectRoot()
30
- const worktreesInfo = await processWorktrees(releaseWorktrees, featureWorktrees, projectRoot)
31
-
32
- logResults(worktreesInfo)
33
-
34
- const structuredContent = {
35
- worktrees: worktreesInfo,
36
- totalCount: worktreesInfo.length,
37
- releaseCount: releaseWorktrees.length,
38
- featureCount: featureWorktrees.length,
39
- }
22
+ if (currentWorktrees.length === 0) {
23
+ logger.info('ℹ️ No active worktrees found')
40
24
 
41
25
  return {
42
- content: [
43
- {
44
- type: 'text',
45
- text: JSON.stringify(structuredContent, null, 2),
46
- },
47
- ],
48
- structuredContent,
49
- }
50
- } catch (error) {
51
- logger.error({ error }, '❌ Error listing worktrees')
52
- throw error
53
- }
54
- }
55
-
56
- /**
57
- * Process worktrees to get detailed information
58
- */
59
- const processWorktrees = async (
60
- releaseWorktrees: string[],
61
- featureWorktrees: string[],
62
- projectRoot: string,
63
- ): Promise<WorktreeInfo[]> => {
64
- const allWorktrees = [
65
- ...releaseWorktrees.map((branch) => {
66
- return { branch, type: 'release' as const }
67
- }),
68
- ...featureWorktrees.map((branch) => {
69
- return { branch, type: 'feature' as const }
70
- }),
71
- ]
72
-
73
- const worktreesInfo: WorktreeInfo[] = []
74
-
75
- for (const { branch, type } of allWorktrees) {
76
- try {
77
- const worktreePath = `${projectRoot}/${branch}`
78
- const isCurrent = await isCurrentWorktree(branch)
79
- const commit = await getWorktreeCommit(worktreePath)
80
- const status = await getWorktreeStatus(worktreePath)
81
- const lastCommitMessage = await getLastCommitMessage(worktreePath)
82
- const aheadBehind = await getAheadBehind(worktreePath)
83
-
84
- worktreesInfo.push({
85
- branch,
86
- path: worktreePath,
87
- commit: commit.substring(0, 8),
88
- isCurrent,
89
- type,
90
- status,
91
- lastCommitMessage: lastCommitMessage.substring(0, 60) + (lastCommitMessage.length > 60 ? '...' : ''),
92
- aheadBehind,
93
- })
94
- } catch (error) {
95
- logger.warn({ error, branch }, `⚠️ Could not process worktree ${branch}`)
96
- }
97
- }
98
-
99
- return worktreesInfo.sort((a, b) => {
100
- // Sort by type first (releases before features), then by branch name
101
- if (a.type !== b.type) {
102
- return a.type === 'release' ? -1 : 1
26
+ content: [{ type: 'text', text: JSON.stringify({ worktrees: [], count: 0 }, null, 2) }],
27
+ structuredContent: { worktrees: [], count: 0 },
103
28
  }
104
-
105
- return a.branch.localeCompare(b.branch)
106
- })
107
- }
108
-
109
- /**
110
- * Check if a worktree is currently active
111
- */
112
- const isCurrentWorktree = async (branch: string): Promise<boolean> => {
113
- try {
114
- const currentBranch = await $`git branch --show-current`
115
-
116
- return currentBranch.stdout.trim() === branch
117
- } catch {
118
- return false
119
- }
120
- }
121
-
122
- /**
123
- * Get the commit hash for a worktree
124
- */
125
- const getWorktreeCommit = async (worktreePath: string): Promise<string> => {
126
- try {
127
- const result = await $`cd ${worktreePath} && git rev-parse HEAD`
128
-
129
- return result.stdout.trim()
130
- } catch {
131
- return 'unknown'
132
- }
133
- }
134
-
135
- /**
136
- * Get the status of a worktree
137
- */
138
- const getWorktreeStatus = async (worktreePath: string): Promise<string> => {
139
- try {
140
- const result = await $`cd ${worktreePath} && git status --porcelain`
141
- const changes = result.stdout.trim().split('\n').filter(Boolean)
142
-
143
- if (changes.length === 0) return 'clean'
144
- if (changes.length <= 3) return 'modified'
145
-
146
- return 'dirty'
147
- } catch {
148
- return 'unknown'
149
- }
150
- }
151
-
152
- /**
153
- * Get the last commit message for a worktree
154
- */
155
- const getLastCommitMessage = async (worktreePath: string): Promise<string> => {
156
- try {
157
- const result = await $`cd ${worktreePath} && git log -1 --pretty=format:"%s"`
158
-
159
- return result.stdout.trim()
160
- } catch {
161
- return 'No commit message available'
162
- }
163
- }
164
-
165
- /**
166
- * Get ahead/behind information for a worktree
167
- */
168
- const getAheadBehind = async (worktreePath: string): Promise<string> => {
169
- try {
170
- const result =
171
- await $`cd ${worktreePath} && git rev-list --count --left-right @{u}...HEAD 2>/dev/null || echo "0 0"`
172
-
173
- const parts = result.stdout.trim().split('\t').map(Number)
174
- const behind = parts[0] || 0
175
- const ahead = parts[1] || 0
176
-
177
- if (ahead === 0 && behind === 0) return 'up to date'
178
- if (ahead > 0 && behind === 0) return `↑${ahead} ahead`
179
- if (behind > 0 && ahead === 0) return `↓${behind} behind`
180
-
181
- return `↑${ahead} ↓${behind}`
182
- } catch {
183
- return 'unknown'
184
- }
185
- }
186
-
187
- /**
188
- * Log the worktrees list in a beautiful formatted way
189
- */
190
- const logResults = (worktrees: WorktreeInfo[]): void => {
191
- if (worktrees.length === 0) {
192
- logger.info('\n🌿 Git Worktrees')
193
- logger.info('─'.repeat(80))
194
- logger.info('ℹ️ No worktrees found')
195
- logger.info('─'.repeat(80))
196
-
197
- return
198
29
  }
199
30
 
200
- logger.info('\n🌿 Git Worktrees')
201
- logger.info('═'.repeat(100))
202
-
203
- // Separate releases and features
204
- const releases = worktrees.filter((w) => {
205
- return w.type === 'release'
206
- })
207
- const features = worktrees.filter((w) => {
208
- return w.type === 'feature'
209
- })
210
-
211
- // Display releases first
212
- displayWorktreeSection('🚀 Releases', releases)
31
+ const [releasePRsInfo, jiraDescriptions] = await Promise.all([getReleasePRsWithInfo(), getJiraDescriptions()])
213
32
 
214
- // Display features second
215
- if (features.length > 0 && releases.length > 0) {
216
- logger.info('')
217
- }
33
+ const releaseTypes = new Map<string, ReleaseType>(
34
+ releasePRsInfo.map((pr) => {
35
+ return [pr.branch, detectReleaseType(pr.title)]
36
+ }),
37
+ )
218
38
 
219
- displayWorktreeSection('✨ Features', features)
39
+ const worktrees: WorktreeInfo[] = currentWorktrees.map((branch) => {
40
+ const version = branch.replace('release/', '')
41
+ const type = releaseTypes.get(branch) || 'regular'
42
+ const description = jiraDescriptions.get(version) || null
220
43
 
221
- // Summary
222
- const current = worktrees.find((w) => {
223
- return w.isCurrent
44
+ return { version, type, description }
224
45
  })
225
46
 
226
- logger.info(`\n${'═'.repeat(100)}`)
227
- logger.info(
228
- `📊 Summary: ${worktrees.length} total worktrees (${releases.length} releases, ${features.length} features)`,
47
+ // Log formatted output
48
+ const maxVersionLength = Math.max(
49
+ ...worktrees.map((w) => {
50
+ return w.version.length
51
+ }),
229
52
  )
230
53
 
231
- if (current) {
232
- logger.info(`📍 Currently on: ${current.branch}`)
233
- }
234
-
235
- logger.info('')
236
- }
237
-
238
- /**
239
- * Display a section of worktrees
240
- */
241
- const displayWorktreeSection = (sectionTitle: string, worktrees: WorktreeInfo[]): void => {
242
- if (worktrees.length === 0) return
243
-
244
- logger.info(`\n${sectionTitle}`)
245
- logger.info('─'.repeat(50))
54
+ const formattedLines = worktrees.map((worktree) => {
55
+ const label = formatVersionLabel(worktree.version, worktree.type, maxVersionLength)
246
56
 
247
- for (const [index, worktree] of worktrees.entries()) {
248
- displayWorktree(worktree)
249
-
250
- if (index < worktrees.length - 1) {
251
- logger.info('')
57
+ if (worktree.description) {
58
+ return `${label} ${worktree.description}`
252
59
  }
253
- }
254
- }
255
-
256
- /**
257
- * Display a single worktree entry
258
- */
259
- const displayWorktree = (worktree: WorktreeInfo): void => {
260
- // Worktree status indicators
261
- const currentIndicator = worktree.isCurrent ? '📍' : ' '
262
- const statusIndicator = getStatusIndicator(worktree.status)
263
-
264
- const typeIcon = worktree.type === 'release' ? '🚀' : '✨'
265
-
266
- // Branch name with color coding
267
- const branchDisplay = `${typeIcon} ${worktree.branch}`
268
-
269
- logger.info(`${currentIndicator} ${statusIndicator} ${branchDisplay}`)
270
-
271
- // Commit and sync info
272
- const syncInfo = worktree.aheadBehind !== 'unknown' ? ` | ${worktree.aheadBehind}` : ''
273
-
274
- logger.info(` 📝 ${worktree.commit}${syncInfo}`)
275
60
 
276
- // Last commit message
277
- logger.info(` 💬 ${worktree.lastCommitMessage}`)
61
+ return label
62
+ })
278
63
 
279
- // Path (shortened for display)
280
- const shortPath = worktree.path.split('/').slice(-2).join('/')
64
+ logger.info('🌿 Active worktrees:')
65
+ logger.info(`\n${formattedLines.join('\n')}\n`)
281
66
 
282
- logger.info(` 📁 ${shortPath}`)
283
- }
67
+ const structuredContent = {
68
+ worktrees,
69
+ count: worktrees.length,
70
+ }
284
71
 
285
- /**
286
- * Get status indicator based on worktree status
287
- */
288
- const getStatusIndicator = (status: string): string => {
289
- switch (status) {
290
- case 'clean':
291
- return '✅'
292
- case 'modified':
293
- return '⚠️ '
294
- case 'dirty':
295
- return '🔴'
296
- default:
297
- return '❓'
72
+ return {
73
+ content: [
74
+ {
75
+ type: 'text',
76
+ text: JSON.stringify(structuredContent, null, 2),
77
+ },
78
+ ],
79
+ structuredContent,
298
80
  }
299
81
  }
300
82
 
@@ -307,20 +89,13 @@ export const worktreesListMcpTool = {
307
89
  worktrees: z
308
90
  .array(
309
91
  z.object({
310
- branch: z.string(),
311
- path: z.string(),
312
- commit: z.string(),
313
- isCurrent: z.boolean(),
314
- type: z.enum(['release', 'feature']),
315
- status: z.string(),
316
- lastCommitMessage: z.string(),
317
- aheadBehind: z.string(),
92
+ version: z.string().describe('Release version'),
93
+ type: z.enum(['regular', 'hotfix']).describe('Release type'),
94
+ description: z.string().nullable().describe('Jira version description'),
318
95
  }),
319
96
  )
320
97
  .describe('List of all worktrees with details'),
321
- totalCount: z.number().describe('Total number of worktrees'),
322
- releaseCount: z.number().describe('Number of release worktrees'),
323
- featureCount: z.number().describe('Number of feature worktrees'),
98
+ count: z.number().describe('Number of worktrees'),
324
99
  },
325
100
  handler: worktreesList,
326
101
  }
@@ -4,15 +4,19 @@ import process from 'node:process'
4
4
  import { z } from 'zod'
5
5
  import { $ } from 'zx'
6
6
 
7
+ import { getReleasePRsWithInfo } from 'src/integrations/gh'
7
8
  import { commandEcho } from 'src/lib/command-echo'
8
9
  import { WORKTREES_DIR_SUFFIX } from 'src/lib/constants'
9
10
  import { getCurrentWorktrees, getProjectRoot } from 'src/lib/git-utils'
10
11
  import { logger } from 'src/lib/logger'
12
+ import { detectReleaseType, formatBranchChoices, getJiraDescriptions } from 'src/lib/release-utils'
13
+ import type { ReleaseType } from 'src/lib/release-utils'
11
14
  import type { RequiredConfirmedOptionArg, ToolsExecutionResult } from 'src/types'
12
15
 
13
16
  // Constants
14
17
  interface WorktreeManagementArgs extends RequiredConfirmedOptionArg {
15
18
  all: boolean
19
+ versions?: string
16
20
  }
17
21
 
18
22
  /**
@@ -20,7 +24,7 @@ interface WorktreeManagementArgs extends RequiredConfirmedOptionArg {
20
24
  * Creates worktrees for active release branches and removes unused ones
21
25
  */
22
26
  export const worktreesRemove = async (options: WorktreeManagementArgs): Promise<ToolsExecutionResult> => {
23
- const { confirmedCommand, all } = options
27
+ const { confirmedCommand, all, versions } = options
24
28
 
25
29
  commandEcho.start('worktrees-remove')
26
30
 
@@ -46,18 +50,25 @@ export const worktreesRemove = async (options: WorktreeManagementArgs): Promise<
46
50
 
47
51
  if (all) {
48
52
  selectedReleaseBranches = currentWorktrees
53
+ } else if (versions) {
54
+ selectedReleaseBranches = versions.split(',').map((v) => {
55
+ return `release/v${v.trim()}`
56
+ })
49
57
  } else {
50
58
  commandEcho.setInteractive()
51
59
 
60
+ const [descriptions, prInfo] = await Promise.all([getJiraDescriptions(), getReleasePRsWithInfo()])
61
+
62
+ const releaseTypes = new Map<string, ReleaseType>(
63
+ prInfo.map((pr) => {
64
+ return [pr.branch, detectReleaseType(pr.title)]
65
+ }),
66
+ )
67
+
52
68
  selectedReleaseBranches = await checkbox({
53
69
  required: true,
54
70
  message: '🌿 Select release branches',
55
- choices: currentWorktrees.map((pr) => {
56
- return {
57
- name: pr.replace('release/v', ''),
58
- value: pr,
59
- }
60
- }),
71
+ choices: formatBranchChoices({ branches: currentWorktrees, descriptions, types: releaseTypes }),
61
72
  })
62
73
  }
63
74
 
@@ -67,7 +78,12 @@ export const worktreesRemove = async (options: WorktreeManagementArgs): Promise<
67
78
  if (allSelected) {
68
79
  commandEcho.addOption('--all', true)
69
80
  } else {
70
- commandEcho.addOption('--branches', selectedReleaseBranches)
81
+ commandEcho.addOption(
82
+ '--versions',
83
+ selectedReleaseBranches.map((branch) => {
84
+ return branch.replace('release/v', '')
85
+ }),
86
+ )
71
87
  }
72
88
 
73
89
  // Ask for confirmation
@@ -87,7 +103,7 @@ export const worktreesRemove = async (options: WorktreeManagementArgs): Promise<
87
103
  }
88
104
 
89
105
  // Track --yes flag if confirmation was interactive (user confirmed)
90
- if (allSelected) {
106
+ if (!confirmedCommand) {
91
107
  commandEcho.addOption('--yes', true)
92
108
  }
93
109
 
@@ -118,22 +134,39 @@ export const worktreesRemove = async (options: WorktreeManagementArgs): Promise<
118
134
  }
119
135
 
120
136
  /**
121
- * Remove worktrees for the specified branches
137
+ * Remove worktrees for the specified branches and whole folder
122
138
  */
123
139
  const removeWorktrees = async (branches: string[], worktreeDir: string): Promise<string[]> => {
124
- const removed: string[] = []
125
-
126
- for (const branch of branches) {
127
- try {
140
+ const results = await Promise.allSettled(
141
+ branches.map(async (branch) => {
128
142
  const worktreePath = `${worktreeDir}/${branch}`
129
143
 
130
144
  await $`git worktree remove ${worktreePath}`
131
- removed.push(branch)
132
- } catch (error) {
133
- logger.error({ error, branch }, `❌ Failed to remove worktree for ${branch}`)
145
+
146
+ return branch
147
+ }),
148
+ )
149
+
150
+ const removed: string[] = []
151
+
152
+ for (const [index, result] of results.entries()) {
153
+ if (result.status === 'fulfilled') {
154
+ removed.push(result.value)
155
+ } else {
156
+ const branch = branches[index]
157
+
158
+ logger.error({ error: result.reason }, `❌ Failed to remove worktree for ${branch}`)
134
159
  }
135
160
  }
136
161
 
162
+ if (removed.length === branches.length) {
163
+ await $`git worktree prune`
164
+ await $`rm -rf ${worktreeDir}`
165
+
166
+ logger.info(`🗑️ Removed worktree folder: ${worktreeDir}`)
167
+ logger.info('')
168
+ }
169
+
137
170
  return removed
138
171
  }
139
172
 
@@ -143,7 +176,9 @@ const removeWorktrees = async (branches: string[], worktreeDir: string): Promise
143
176
  const logResults = (removed: string[]): void => {
144
177
  if (removed.length > 0) {
145
178
  logger.info('❌ Removed worktrees:')
146
- logger.info(removed.join('\n'))
179
+ for (const branch of removed) {
180
+ logger.info(branch)
181
+ }
147
182
  logger.info('')
148
183
  } else {
149
184
  logger.info('ℹ️ No unused worktrees to remove')
@@ -155,11 +190,12 @@ export const worktreesRemoveMcpTool = {
155
190
  name: 'worktrees-remove',
156
191
  description: 'Remove selected worktrees',
157
192
  inputSchema: {
158
- all: z.boolean().describe('Remove all worktrees without prompting'),
193
+ all: z.boolean().describe('Remove all git worktrees without prompting'),
194
+ versions: z.string().optional().describe('Specify versions by comma, e.g. 1.2.5, 1.2.6'),
159
195
  },
160
196
  outputSchema: {
161
- removedWorktrees: z.array(z.string()).describe('List of removed worktree branches'),
162
- count: z.number().describe('Number of worktrees removed'),
197
+ removedWorktrees: z.array(z.string()).describe('List of removed git worktree branches'),
198
+ count: z.number().describe('Number of git worktrees removed'),
163
199
  },
164
200
  handler: worktreesRemove,
165
201
  }
@@ -133,7 +133,9 @@ const removeWorktrees = async (branches: string[], worktreeDir: string): Promise
133
133
  const logResults = (removed: string[]): void => {
134
134
  if (removed.length > 0) {
135
135
  logger.info('❌ Removed worktrees:')
136
- logger.info(removed.join('\n'))
136
+ for (const branch of removed) {
137
+ logger.info(branch)
138
+ }
137
139
  logger.info('')
138
140
  } else {
139
141
  logger.info('ℹ️ No unused worktrees to remove')
package/src/entry/cli.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Command } from 'commander'
1
+ import { Command, Option } from 'commander'
2
2
 
3
3
  import { doctor } from 'src/commands/doctor'
4
4
  import { envClear } from 'src/commands/env-clear'
@@ -6,7 +6,6 @@ import { envInit } from 'src/commands/env-init'
6
6
  import { envList } from 'src/commands/env-list'
7
7
  import { envLoad } from 'src/commands/env-load'
8
8
  import { envStatus } from 'src/commands/env-status'
9
- // Commands
10
9
  import { ghMergeDev } from 'src/commands/gh-merge-dev'
11
10
  import { ghReleaseDeliver } from 'src/commands/gh-release-deliver'
12
11
  import { ghReleaseDeployAll } from 'src/commands/gh-release-deploy-all'
@@ -43,12 +42,14 @@ program
43
42
  .description('Create a single release branch')
44
43
  .option('-v, --version <version>', 'Specify the version to create, e.g. 1.2.5')
45
44
  .option('-d, --description <description>', 'Optional description for the Jira version')
45
+ .addOption(new Option('-t, --type <type>', 'Release type (default: regular)').choices(['regular', 'hotfix']))
46
46
  .option('-y, --yes', 'Skip confirmation prompt')
47
47
  .option('--no-checkout', 'Do not checkout the created branch after creation (checkout is default)')
48
48
  .action(async (options) => {
49
49
  await releaseCreate({
50
50
  version: options.version,
51
51
  description: options.description,
52
+ type: options.type,
52
53
  confirmedCommand: options.yes,
53
54
  checkout: options.checkout,
54
55
  })
@@ -58,12 +59,12 @@ program
58
59
  .command('release-create-batch')
59
60
  .description('Create multiple release branches (batch operation)')
60
61
  .option('-v, --versions <versions>', 'Specify the versions to create by comma, e.g. 1.2.5, 1.2.6')
61
- .option('-d, --description <description>', 'Optional description for the Jira versions')
62
+ .addOption(new Option('-t, --type <type>', 'Release type (default: regular)').choices(['regular', 'hotfix']))
62
63
  .option('-y, --yes', 'Skip confirmation prompt')
63
64
  .action(async (options) => {
64
65
  await releaseCreateBatch({
65
66
  versions: options.versions,
66
- description: options.description,
67
+ type: options.type,
67
68
  confirmedCommand: options.yes,
68
69
  })
69
70
  })
@@ -132,9 +133,19 @@ program
132
133
  .description('Add git worktrees for release branches')
133
134
  .option('-y, --yes', 'Skip confirmation prompt')
134
135
  .option('-a, --all', 'Select all active release branches')
136
+ .option('-v, --versions <versions>', 'Specify versions by comma, e.g. 1.2.5, 1.2.6')
135
137
  .option('-c, --cursor', 'Open created worktrees in Cursor')
138
+ .option('--no-cursor', 'Skip Cursor prompt')
139
+ .option('-g, --github-desktop', 'Open created worktrees in GitHub Desktop')
140
+ .option('--no-github-desktop', 'Skip GitHub Desktop prompt')
136
141
  .action(async (options) => {
137
- await worktreesAdd({ confirmedCommand: options.yes, all: options.all, cursor: options.cursor })
142
+ await worktreesAdd({
143
+ confirmedCommand: options.yes,
144
+ all: options.all,
145
+ versions: options.versions,
146
+ cursor: options.cursor,
147
+ githubDesktop: options.githubDesktop,
148
+ })
138
149
  })
139
150
 
140
151
  program
@@ -149,8 +160,9 @@ program
149
160
  .description('Remove git worktrees for release branches')
150
161
  .option('-y, --yes', 'Skip confirmation prompt')
151
162
  .option('-a, --all', 'Select all active release branches')
163
+ .option('-v, --versions <versions>', 'Specify versions by comma, e.g. 1.2.5, 1.2.6')
152
164
  .action(async (options) => {
153
- await worktreesRemove({ confirmedCommand: options.yes, all: options.all })
165
+ await worktreesRemove({ confirmedCommand: options.yes, all: options.all, versions: options.versions })
154
166
  })
155
167
 
156
168
  program
@@ -183,9 +195,7 @@ program
183
195
 
184
196
  program
185
197
  .command('env-load')
186
- .description(
187
- 'Load Doppler env vars for a config. Prints a file path to stdout — auto-sourced by the env-load shell alias, or run source <path> manually',
188
- )
198
+ .description('Load Doppler env vars for a config. Source the returned file path to apply.')
189
199
  .option('-c, --config <config>', 'Environment config name to load (e.g. dev, arthur)')
190
200
  .action(async (options) => {
191
201
  await envLoad({ config: options.config })
@@ -193,9 +203,7 @@ program
193
203
 
194
204
  program
195
205
  .command('env-clear')
196
- .description(
197
- 'Clear loaded env vars. Prints a file path to stdout — auto-sourced by the env-clear shell alias, or run source <path> manually',
198
- )
206
+ .description('Clear loaded env vars. Source the returned file path to apply.')
199
207
  .action(async () => {
200
208
  await envClear()
201
209
  })