prjct-cli 0.45.0 → 0.45.4

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.
Files changed (207) hide show
  1. package/CHANGELOG.md +82 -0
  2. package/bin/prjct.ts +117 -10
  3. package/core/__tests__/agentic/memory-system.test.ts +39 -26
  4. package/core/__tests__/agentic/plan-mode.test.ts +64 -46
  5. package/core/__tests__/agentic/prompt-builder.test.ts +14 -14
  6. package/core/__tests__/services/project-index.test.ts +353 -0
  7. package/core/__tests__/types/fs.test.ts +3 -3
  8. package/core/__tests__/utils/date-helper.test.ts +10 -10
  9. package/core/__tests__/utils/output.test.ts +9 -6
  10. package/core/__tests__/utils/project-commands.test.ts +5 -6
  11. package/core/agentic/agent-router.ts +9 -10
  12. package/core/agentic/chain-of-thought.ts +16 -4
  13. package/core/agentic/command-executor.ts +66 -40
  14. package/core/agentic/context-builder.ts +8 -5
  15. package/core/agentic/ground-truth.ts +15 -9
  16. package/core/agentic/index.ts +145 -152
  17. package/core/agentic/loop-detector.ts +40 -11
  18. package/core/agentic/memory-system.ts +98 -35
  19. package/core/agentic/orchestrator-executor.ts +135 -71
  20. package/core/agentic/plan-mode.ts +46 -16
  21. package/core/agentic/prompt-builder.ts +108 -42
  22. package/core/agentic/services.ts +10 -9
  23. package/core/agentic/skill-loader.ts +9 -15
  24. package/core/agentic/smart-context.ts +129 -79
  25. package/core/agentic/template-executor.ts +13 -12
  26. package/core/agentic/template-loader.ts +7 -4
  27. package/core/agentic/tool-registry.ts +16 -13
  28. package/core/agents/index.ts +1 -1
  29. package/core/agents/performance.ts +10 -27
  30. package/core/ai-tools/formatters.ts +8 -6
  31. package/core/ai-tools/generator.ts +4 -4
  32. package/core/ai-tools/index.ts +1 -1
  33. package/core/ai-tools/registry.ts +21 -11
  34. package/core/bus/bus.ts +23 -16
  35. package/core/bus/index.ts +2 -2
  36. package/core/cli/linear.ts +3 -5
  37. package/core/cli/start.ts +28 -25
  38. package/core/commands/analysis.ts +58 -39
  39. package/core/commands/analytics.ts +52 -44
  40. package/core/commands/base.ts +15 -13
  41. package/core/commands/cleanup.ts +6 -13
  42. package/core/commands/command-data.ts +28 -4
  43. package/core/commands/commands.ts +57 -24
  44. package/core/commands/context.ts +4 -4
  45. package/core/commands/design.ts +3 -10
  46. package/core/commands/index.ts +5 -8
  47. package/core/commands/maintenance.ts +7 -4
  48. package/core/commands/planning.ts +179 -56
  49. package/core/commands/register.ts +13 -9
  50. package/core/commands/registry.ts +15 -14
  51. package/core/commands/setup.ts +26 -14
  52. package/core/commands/shipping.ts +11 -16
  53. package/core/commands/snapshots.ts +16 -32
  54. package/core/commands/uninstall.ts +541 -0
  55. package/core/commands/workflow.ts +24 -28
  56. package/core/constants/index.ts +10 -22
  57. package/core/context/generator.ts +82 -33
  58. package/core/context-tools/files-tool.ts +18 -19
  59. package/core/context-tools/imports-tool.ts +13 -33
  60. package/core/context-tools/index.ts +29 -54
  61. package/core/context-tools/recent-tool.ts +16 -22
  62. package/core/context-tools/signatures-tool.ts +17 -26
  63. package/core/context-tools/summary-tool.ts +20 -22
  64. package/core/context-tools/token-counter.ts +25 -20
  65. package/core/context-tools/types.ts +5 -5
  66. package/core/domain/agent-generator.ts +7 -5
  67. package/core/domain/agent-loader.ts +2 -2
  68. package/core/domain/analyzer.ts +19 -16
  69. package/core/domain/architecture-generator.ts +6 -3
  70. package/core/domain/context-estimator.ts +3 -4
  71. package/core/domain/snapshot-manager.ts +25 -22
  72. package/core/domain/task-stack.ts +24 -14
  73. package/core/errors.ts +1 -1
  74. package/core/events/events.ts +2 -4
  75. package/core/events/index.ts +1 -2
  76. package/core/index.ts +28 -16
  77. package/core/infrastructure/agent-detector.ts +3 -3
  78. package/core/infrastructure/ai-provider.ts +23 -20
  79. package/core/infrastructure/author-detector.ts +16 -10
  80. package/core/infrastructure/capability-installer.ts +2 -2
  81. package/core/infrastructure/claude-agent.ts +6 -6
  82. package/core/infrastructure/command-installer.ts +22 -17
  83. package/core/infrastructure/config-manager.ts +18 -14
  84. package/core/infrastructure/editors-config.ts +8 -4
  85. package/core/infrastructure/path-manager.ts +8 -6
  86. package/core/infrastructure/permission-manager.ts +20 -17
  87. package/core/infrastructure/setup.ts +42 -38
  88. package/core/infrastructure/update-checker.ts +5 -5
  89. package/core/integrations/issue-tracker/enricher.ts +8 -19
  90. package/core/integrations/issue-tracker/index.ts +2 -2
  91. package/core/integrations/issue-tracker/manager.ts +15 -15
  92. package/core/integrations/issue-tracker/types.ts +5 -22
  93. package/core/integrations/jira/client.ts +67 -59
  94. package/core/integrations/jira/index.ts +11 -14
  95. package/core/integrations/jira/mcp-adapter.ts +5 -10
  96. package/core/integrations/jira/service.ts +10 -10
  97. package/core/integrations/linear/client.ts +27 -18
  98. package/core/integrations/linear/index.ts +9 -12
  99. package/core/integrations/linear/service.ts +11 -11
  100. package/core/integrations/linear/sync.ts +8 -8
  101. package/core/outcomes/analyzer.ts +5 -18
  102. package/core/outcomes/index.ts +2 -2
  103. package/core/outcomes/recorder.ts +3 -3
  104. package/core/plugin/builtin/webhook.ts +19 -15
  105. package/core/plugin/hooks.ts +29 -21
  106. package/core/plugin/index.ts +7 -7
  107. package/core/plugin/loader.ts +19 -19
  108. package/core/plugin/registry.ts +12 -23
  109. package/core/schemas/agents.ts +1 -1
  110. package/core/schemas/analysis.ts +1 -1
  111. package/core/schemas/enriched-task.ts +62 -49
  112. package/core/schemas/ideas.ts +13 -13
  113. package/core/schemas/index.ts +17 -27
  114. package/core/schemas/issues.ts +40 -25
  115. package/core/schemas/metrics.ts +25 -25
  116. package/core/schemas/outcomes.ts +70 -62
  117. package/core/schemas/permissions.ts +15 -12
  118. package/core/schemas/prd.ts +27 -14
  119. package/core/schemas/project.ts +3 -3
  120. package/core/schemas/roadmap.ts +47 -34
  121. package/core/schemas/schemas.ts +3 -4
  122. package/core/schemas/shipped.ts +3 -3
  123. package/core/schemas/state.ts +43 -29
  124. package/core/server/index.ts +5 -6
  125. package/core/server/routes-extended.ts +68 -72
  126. package/core/server/routes.ts +3 -3
  127. package/core/server/server.ts +31 -26
  128. package/core/services/agent-generator.ts +237 -0
  129. package/core/services/agent-service.ts +2 -2
  130. package/core/services/breakdown-service.ts +2 -4
  131. package/core/services/context-generator.ts +299 -0
  132. package/core/services/context-selector.ts +420 -0
  133. package/core/services/doctor-service.ts +426 -0
  134. package/core/services/file-categorizer.ts +448 -0
  135. package/core/services/file-scorer.ts +270 -0
  136. package/core/services/git-analyzer.ts +267 -0
  137. package/core/services/index.ts +27 -10
  138. package/core/services/memory-service.ts +3 -4
  139. package/core/services/project-index.ts +911 -0
  140. package/core/services/project-service.ts +4 -4
  141. package/core/services/skill-installer.ts +14 -17
  142. package/core/services/skill-lock.ts +3 -3
  143. package/core/services/skill-service.ts +12 -6
  144. package/core/services/stack-detector.ts +245 -0
  145. package/core/services/sync-service.ts +87 -345
  146. package/core/services/watch-service.ts +294 -0
  147. package/core/session/compaction.ts +23 -31
  148. package/core/session/index.ts +11 -5
  149. package/core/session/log-migration.ts +3 -3
  150. package/core/session/metrics.ts +19 -14
  151. package/core/session/session-log-manager.ts +12 -17
  152. package/core/session/task-session-manager.ts +25 -25
  153. package/core/session/utils.ts +1 -1
  154. package/core/storage/ideas-storage.ts +41 -57
  155. package/core/storage/index-storage.ts +514 -0
  156. package/core/storage/index.ts +41 -17
  157. package/core/storage/metrics-storage.ts +39 -34
  158. package/core/storage/queue-storage.ts +35 -45
  159. package/core/storage/shipped-storage.ts +17 -20
  160. package/core/storage/state-storage.ts +50 -30
  161. package/core/storage/storage-manager.ts +6 -6
  162. package/core/storage/storage.ts +18 -15
  163. package/core/sync/auth-config.ts +3 -3
  164. package/core/sync/index.ts +13 -19
  165. package/core/sync/oauth-handler.ts +3 -3
  166. package/core/sync/sync-client.ts +4 -9
  167. package/core/sync/sync-manager.ts +12 -14
  168. package/core/types/commands.ts +42 -7
  169. package/core/types/index.ts +284 -305
  170. package/core/types/integrations.ts +3 -3
  171. package/core/types/storage.ts +14 -14
  172. package/core/types/utils.ts +3 -3
  173. package/core/utils/agent-stream.ts +3 -1
  174. package/core/utils/animations.ts +14 -11
  175. package/core/utils/branding.ts +7 -7
  176. package/core/utils/cache.ts +1 -3
  177. package/core/utils/collection-filters.ts +3 -15
  178. package/core/utils/date-helper.ts +2 -7
  179. package/core/utils/file-helper.ts +13 -8
  180. package/core/utils/jsonl-helper.ts +13 -10
  181. package/core/utils/keychain.ts +4 -8
  182. package/core/utils/logger.ts +1 -1
  183. package/core/utils/next-steps.ts +3 -3
  184. package/core/utils/output.ts +58 -11
  185. package/core/utils/project-commands.ts +6 -6
  186. package/core/utils/project-credentials.ts +5 -12
  187. package/core/utils/runtime.ts +2 -2
  188. package/core/utils/session-helper.ts +3 -4
  189. package/core/utils/version.ts +3 -3
  190. package/core/wizard/index.ts +13 -0
  191. package/core/wizard/onboarding.ts +633 -0
  192. package/core/workflow/state-machine.ts +7 -7
  193. package/dist/bin/prjct.mjs +18755 -15574
  194. package/dist/core/infrastructure/command-installer.js +86 -79
  195. package/dist/core/infrastructure/editors-config.js +6 -6
  196. package/dist/core/infrastructure/setup.js +246 -225
  197. package/dist/core/utils/version.js +9 -9
  198. package/package.json +11 -12
  199. package/scripts/build.js +3 -3
  200. package/scripts/postinstall.js +2 -2
  201. package/templates/mcp-config.json +6 -1
  202. package/templates/permissions/permissive.jsonc +1 -1
  203. package/templates/permissions/strict.jsonc +5 -9
  204. package/templates/global/docs/agents.md +0 -88
  205. package/templates/global/docs/architecture.md +0 -103
  206. package/templates/global/docs/commands.md +0 -96
  207. package/templates/global/docs/validation.md +0 -95
@@ -4,18 +4,11 @@
4
4
  * Git-based undo/redo functionality and session recovery.
5
5
  */
6
6
 
7
- import path from 'path'
8
-
9
- import { isNotFoundError } from '../types/fs'
10
- import type { CommandResult } from '../types'
11
- import {
12
- pathManager,
13
- configManager,
14
- fileHelper,
15
- dateHelper,
16
- out
17
- } from './base'
7
+ import path from 'node:path'
18
8
  import { memoryService } from '../services'
9
+ import type { CommandResult } from '../types'
10
+ import { isNotFoundError } from '../types/fs'
11
+ import { configManager, dateHelper, fileHelper, out, pathManager } from './base'
19
12
 
20
13
  interface SnapshotHistory {
21
14
  snapshots: { id: string; timestamp: string; message: string }[]
@@ -97,19 +90,16 @@ export async function undo(projectPath: string = process.cwd()): Promise<Command
97
90
  }
98
91
 
99
92
  // Create snapshots directory
100
- const snapshotsPath = path.join(
101
- pathManager.getGlobalProjectPath(projectId),
102
- 'snapshots'
103
- )
93
+ const snapshotsPath = path.join(pathManager.getGlobalProjectPath(projectId), 'snapshots')
104
94
  await fileHelper.ensureDir(snapshotsPath)
105
95
 
106
96
  // Check git status
107
- const { execSync } = await import('child_process')
97
+ const { execSync } = await import('node:child_process')
108
98
 
109
99
  try {
110
100
  const status = execSync('git status --porcelain', {
111
101
  cwd: projectPath,
112
- encoding: 'utf-8'
102
+ encoding: 'utf-8',
113
103
  }).trim()
114
104
 
115
105
  if (!status) {
@@ -123,7 +113,7 @@ export async function undo(projectPath: string = process.cwd()): Promise<Command
123
113
 
124
114
  execSync(`git stash push -m "${stashMessage}"`, {
125
115
  cwd: projectPath,
126
- encoding: 'utf-8'
116
+ encoding: 'utf-8',
127
117
  })
128
118
 
129
119
  // Save snapshot metadata
@@ -143,7 +133,7 @@ export async function undo(projectPath: string = process.cwd()): Promise<Command
143
133
  history.snapshots.push({
144
134
  id: stashMessage,
145
135
  timestamp: new Date().toISOString(),
146
- message: stashMessage
136
+ message: stashMessage,
147
137
  })
148
138
  history.current = history.snapshots.length - 1
149
139
 
@@ -179,10 +169,7 @@ export async function redo(projectPath: string = process.cwd()): Promise<Command
179
169
  return { success: false, error: 'No project ID found' }
180
170
  }
181
171
 
182
- const snapshotsPath = path.join(
183
- pathManager.getGlobalProjectPath(projectId),
184
- 'snapshots'
185
- )
172
+ const snapshotsPath = path.join(pathManager.getGlobalProjectPath(projectId), 'snapshots')
186
173
  const snapshotFile = path.join(snapshotsPath, 'history.json')
187
174
 
188
175
  let history: SnapshotHistory
@@ -203,13 +190,13 @@ export async function redo(projectPath: string = process.cwd()): Promise<Command
203
190
  return { success: false, message: 'Nothing to redo' }
204
191
  }
205
192
 
206
- const { execSync } = await import('child_process')
193
+ const { execSync } = await import('node:child_process')
207
194
 
208
195
  try {
209
196
  // Get latest stash
210
197
  const stashList = execSync('git stash list', {
211
198
  cwd: projectPath,
212
- encoding: 'utf-8'
199
+ encoding: 'utf-8',
213
200
  }).trim()
214
201
 
215
202
  if (!stashList) {
@@ -218,7 +205,7 @@ export async function redo(projectPath: string = process.cwd()): Promise<Command
218
205
  }
219
206
 
220
207
  // Find prjct stash
221
- const prjctStash = stashList.split('\n').find(line => line.includes('prjct-undo-'))
208
+ const prjctStash = stashList.split('\n').find((line) => line.includes('prjct-undo-'))
222
209
 
223
210
  if (!prjctStash) {
224
211
  out.warn('no prjct undo point found')
@@ -228,7 +215,7 @@ export async function redo(projectPath: string = process.cwd()): Promise<Command
228
215
  // Pop the stash
229
216
  execSync('git stash pop', {
230
217
  cwd: projectPath,
231
- encoding: 'utf-8'
218
+ encoding: 'utf-8',
232
219
  })
233
220
 
234
221
  // Remove from history
@@ -264,10 +251,7 @@ export async function history(projectPath: string = process.cwd()): Promise<Comm
264
251
  return { success: false, error: 'No project ID found' }
265
252
  }
266
253
 
267
- const snapshotsPath = path.join(
268
- pathManager.getGlobalProjectPath(projectId),
269
- 'snapshots'
270
- )
254
+ const snapshotsPath = path.join(pathManager.getGlobalProjectPath(projectId), 'snapshots')
271
255
  const snapshotFile = path.join(snapshotsPath, 'history.json')
272
256
 
273
257
  let snapshotHistory: SnapshotHistory
@@ -303,7 +287,7 @@ export async function history(projectPath: string = process.cwd()): Promise<Comm
303
287
  console.log(' Use /p:redo to restore the latest\n')
304
288
  }
305
289
 
306
- console.log('='.repeat(50) + '\n')
290
+ console.log(`${'='.repeat(50)}\n`)
307
291
 
308
292
  return { success: true, snapshots: snapshotHistory.snapshots, current: snapshotHistory.current }
309
293
  } catch (error) {
@@ -0,0 +1,541 @@
1
+ /**
2
+ * Uninstall Command
3
+ *
4
+ * Complete system removal of prjct-cli.
5
+ * Handles cleanup of all prjct files, configurations, and installations.
6
+ *
7
+ * @version 1.0.0
8
+ */
9
+
10
+ import { execSync } from 'node:child_process'
11
+ import fsSync from 'node:fs'
12
+ import fs from 'node:fs/promises'
13
+ import os from 'node:os'
14
+ import path from 'node:path'
15
+ import readline from 'node:readline'
16
+ import chalk from 'chalk'
17
+ import { getProviderPaths } from '../infrastructure/command-installer'
18
+ import pathManager from '../infrastructure/path-manager'
19
+ import type { CommandResult, UninstallOptions } from '../types'
20
+ import { PrjctCommandsBase } from './base'
21
+
22
+ // Markers for prjct section in CLAUDE.md
23
+ const PRJCT_START_MARKER = '<!-- prjct:start - DO NOT REMOVE THIS MARKER -->'
24
+ const PRJCT_END_MARKER = '<!-- prjct:end - DO NOT REMOVE THIS MARKER -->'
25
+
26
+ interface UninstallItem {
27
+ path: string
28
+ type: 'directory' | 'file' | 'section'
29
+ description: string
30
+ size?: number
31
+ count?: number
32
+ exists: boolean
33
+ }
34
+
35
+ interface InstallationInfo {
36
+ homebrew: boolean
37
+ npm: boolean
38
+ homebrewFormula?: string
39
+ }
40
+
41
+ /**
42
+ * Get directory size recursively
43
+ */
44
+ async function getDirectorySize(dirPath: string): Promise<number> {
45
+ let totalSize = 0
46
+
47
+ try {
48
+ const entries = await fs.readdir(dirPath, { withFileTypes: true })
49
+
50
+ for (const entry of entries) {
51
+ const entryPath = path.join(dirPath, entry.name)
52
+
53
+ if (entry.isDirectory()) {
54
+ totalSize += await getDirectorySize(entryPath)
55
+ } else {
56
+ try {
57
+ const stats = await fs.stat(entryPath)
58
+ totalSize += stats.size
59
+ } catch {
60
+ // Skip files we can't stat
61
+ }
62
+ }
63
+ }
64
+ } catch {
65
+ // Directory doesn't exist or can't be read
66
+ }
67
+
68
+ return totalSize
69
+ }
70
+
71
+ /**
72
+ * Format bytes to human readable size
73
+ */
74
+ function formatSize(bytes: number): string {
75
+ if (bytes === 0) return '0 B'
76
+
77
+ const units = ['B', 'KB', 'MB', 'GB']
78
+ const i = Math.floor(Math.log(bytes) / Math.log(1024))
79
+ const size = bytes / 1024 ** i
80
+
81
+ return `${size.toFixed(1)} ${units[i]}`
82
+ }
83
+
84
+ /**
85
+ * Count items in a directory
86
+ */
87
+ async function countDirectoryItems(dirPath: string): Promise<number> {
88
+ try {
89
+ const entries = await fs.readdir(dirPath, { withFileTypes: true })
90
+ return entries.filter((e) => e.isDirectory()).length
91
+ } catch {
92
+ return 0
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Detect installation method
98
+ */
99
+ function detectInstallation(): InstallationInfo {
100
+ const info: InstallationInfo = {
101
+ homebrew: false,
102
+ npm: false,
103
+ }
104
+
105
+ // Check Homebrew
106
+ try {
107
+ const result = execSync('brew list prjct-cli 2>/dev/null', { encoding: 'utf-8' })
108
+ if (result) {
109
+ info.homebrew = true
110
+ info.homebrewFormula = 'prjct-cli'
111
+ }
112
+ } catch {
113
+ // Not installed via Homebrew
114
+ }
115
+
116
+ // Check npm global
117
+ try {
118
+ const result = execSync('npm list -g prjct-cli --depth=0 2>/dev/null', { encoding: 'utf-8' })
119
+ if (result.includes('prjct-cli')) {
120
+ info.npm = true
121
+ }
122
+ } catch {
123
+ // Not installed via npm
124
+ }
125
+
126
+ return info
127
+ }
128
+
129
+ /**
130
+ * Gather all items to uninstall
131
+ */
132
+ async function gatherUninstallItems(): Promise<UninstallItem[]> {
133
+ const items: UninstallItem[] = []
134
+ const providerPaths = getProviderPaths()
135
+
136
+ // 1. ~/.prjct-cli/ (main data directory)
137
+ const prjctCliPath = pathManager.getGlobalBasePath()
138
+ const prjctCliExists = fsSync.existsSync(prjctCliPath)
139
+ const projectCount = prjctCliExists
140
+ ? await countDirectoryItems(path.join(prjctCliPath, 'projects'))
141
+ : 0
142
+ const prjctCliSize = prjctCliExists ? await getDirectorySize(prjctCliPath) : 0
143
+
144
+ items.push({
145
+ path: prjctCliPath,
146
+ type: 'directory',
147
+ description: `All project data${projectCount > 0 ? `, ${projectCount} project${projectCount > 1 ? 's' : ''}` : ''}`,
148
+ size: prjctCliSize,
149
+ count: projectCount,
150
+ exists: prjctCliExists,
151
+ })
152
+
153
+ // 2. ~/.claude/CLAUDE.md (prjct section only)
154
+ const claudeMdPath = path.join(providerPaths.claude.config, 'CLAUDE.md')
155
+ const claudeMdExists = fsSync.existsSync(claudeMdPath)
156
+ let hasPrjctSection = false
157
+
158
+ if (claudeMdExists) {
159
+ try {
160
+ const content = fsSync.readFileSync(claudeMdPath, 'utf-8')
161
+ hasPrjctSection = content.includes(PRJCT_START_MARKER) && content.includes(PRJCT_END_MARKER)
162
+ } catch {
163
+ // Can't read file
164
+ }
165
+ }
166
+
167
+ items.push({
168
+ path: claudeMdPath,
169
+ type: 'section',
170
+ description: 'prjct section in CLAUDE.md',
171
+ exists: claudeMdExists && hasPrjctSection,
172
+ })
173
+
174
+ // 3. ~/.claude/commands/p/ (prjct commands)
175
+ const claudeCommandsPath = providerPaths.claude.commands
176
+ const claudeCommandsExists = fsSync.existsSync(claudeCommandsPath)
177
+ const claudeCommandsSize = claudeCommandsExists ? await getDirectorySize(claudeCommandsPath) : 0
178
+
179
+ items.push({
180
+ path: claudeCommandsPath,
181
+ type: 'directory',
182
+ description: 'Claude commands',
183
+ size: claudeCommandsSize,
184
+ exists: claudeCommandsExists,
185
+ })
186
+
187
+ // 4. ~/.claude/commands/p.md (router)
188
+ const claudeRouterPath = providerPaths.claude.router
189
+ const claudeRouterExists = fsSync.existsSync(claudeRouterPath)
190
+
191
+ items.push({
192
+ path: claudeRouterPath,
193
+ type: 'file',
194
+ description: 'Claude router',
195
+ exists: claudeRouterExists,
196
+ })
197
+
198
+ // 5. ~/.claude/prjct-statusline.sh (status line script)
199
+ const statusLinePath = path.join(providerPaths.claude.config, 'prjct-statusline.sh')
200
+ const statusLineExists = fsSync.existsSync(statusLinePath)
201
+
202
+ items.push({
203
+ path: statusLinePath,
204
+ type: 'file',
205
+ description: 'Status line script',
206
+ exists: statusLineExists,
207
+ })
208
+
209
+ // 6. ~/.gemini/commands/p.toml (Gemini router, if exists)
210
+ const geminiRouterPath = providerPaths.gemini.router
211
+ const geminiRouterExists = fsSync.existsSync(geminiRouterPath)
212
+
213
+ items.push({
214
+ path: geminiRouterPath,
215
+ type: 'file',
216
+ description: 'Gemini router',
217
+ exists: geminiRouterExists,
218
+ })
219
+
220
+ // 7. ~/.gemini/GEMINI.md (prjct section only, if exists)
221
+ const geminiMdPath = path.join(providerPaths.gemini.config, 'GEMINI.md')
222
+ const geminiMdExists = fsSync.existsSync(geminiMdPath)
223
+ let hasGeminiPrjctSection = false
224
+
225
+ if (geminiMdExists) {
226
+ try {
227
+ const content = fsSync.readFileSync(geminiMdPath, 'utf-8')
228
+ hasGeminiPrjctSection =
229
+ content.includes(PRJCT_START_MARKER) && content.includes(PRJCT_END_MARKER)
230
+ } catch {
231
+ // Can't read file
232
+ }
233
+ }
234
+
235
+ if (geminiMdExists && hasGeminiPrjctSection) {
236
+ items.push({
237
+ path: geminiMdPath,
238
+ type: 'section',
239
+ description: 'prjct section in GEMINI.md',
240
+ exists: true,
241
+ })
242
+ }
243
+
244
+ return items
245
+ }
246
+
247
+ /**
248
+ * Remove prjct section from a markdown file
249
+ */
250
+ async function removePrjctSection(filePath: string): Promise<boolean> {
251
+ try {
252
+ const content = await fs.readFile(filePath, 'utf-8')
253
+
254
+ if (!content.includes(PRJCT_START_MARKER) || !content.includes(PRJCT_END_MARKER)) {
255
+ return false
256
+ }
257
+
258
+ const startIndex = content.indexOf(PRJCT_START_MARKER)
259
+ const endIndex = content.indexOf(PRJCT_END_MARKER) + PRJCT_END_MARKER.length
260
+
261
+ // Remove the section and any trailing newlines
262
+ let newContent = content.substring(0, startIndex) + content.substring(endIndex)
263
+ newContent = newContent.replace(/\n{3,}/g, '\n\n').trim()
264
+
265
+ // If the file is now empty or just whitespace, delete it
266
+ if (!newContent || newContent.trim().length === 0) {
267
+ await fs.unlink(filePath)
268
+ } else {
269
+ await fs.writeFile(filePath, `${newContent}\n`, 'utf-8')
270
+ }
271
+
272
+ return true
273
+ } catch {
274
+ return false
275
+ }
276
+ }
277
+
278
+ /**
279
+ * Create backup of prjct data
280
+ */
281
+ async function createBackup(): Promise<string | null> {
282
+ const homeDir = os.homedir()
283
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-').substring(0, 19)
284
+ const backupDir = path.join(homeDir, `.prjct-backup-${timestamp}`)
285
+
286
+ try {
287
+ await fs.mkdir(backupDir, { recursive: true })
288
+
289
+ const prjctCliPath = pathManager.getGlobalBasePath()
290
+
291
+ if (fsSync.existsSync(prjctCliPath)) {
292
+ // Copy entire .prjct-cli directory
293
+ await copyDirectory(prjctCliPath, path.join(backupDir, '.prjct-cli'))
294
+ }
295
+
296
+ return backupDir
297
+ } catch {
298
+ return null
299
+ }
300
+ }
301
+
302
+ /**
303
+ * Recursively copy a directory
304
+ */
305
+ async function copyDirectory(src: string, dest: string): Promise<void> {
306
+ await fs.mkdir(dest, { recursive: true })
307
+ const entries = await fs.readdir(src, { withFileTypes: true })
308
+
309
+ for (const entry of entries) {
310
+ const srcPath = path.join(src, entry.name)
311
+ const destPath = path.join(dest, entry.name)
312
+
313
+ if (entry.isDirectory()) {
314
+ await copyDirectory(srcPath, destPath)
315
+ } else {
316
+ await fs.copyFile(srcPath, destPath)
317
+ }
318
+ }
319
+ }
320
+
321
+ /**
322
+ * Perform the actual uninstallation
323
+ */
324
+ async function performUninstall(
325
+ items: UninstallItem[],
326
+ installation: InstallationInfo,
327
+ options: UninstallOptions
328
+ ): Promise<{ deleted: string[]; errors: string[] }> {
329
+ const deleted: string[] = []
330
+ const errors: string[] = []
331
+
332
+ for (const item of items) {
333
+ if (!item.exists) continue
334
+
335
+ try {
336
+ if (item.type === 'section') {
337
+ // Remove prjct section from file
338
+ const success = await removePrjctSection(item.path)
339
+ if (success) {
340
+ deleted.push(item.path)
341
+ }
342
+ } else if (item.type === 'directory') {
343
+ await fs.rm(item.path, { recursive: true, force: true })
344
+ deleted.push(item.path)
345
+ } else if (item.type === 'file') {
346
+ await fs.unlink(item.path)
347
+ deleted.push(item.path)
348
+ }
349
+ } catch (error) {
350
+ errors.push(`${item.path}: ${(error as Error).message}`)
351
+ }
352
+ }
353
+
354
+ // Uninstall package managers
355
+ if (!options.keepPackage) {
356
+ if (installation.homebrew && installation.homebrewFormula) {
357
+ try {
358
+ if (!options.dryRun) {
359
+ execSync(`brew uninstall ${installation.homebrewFormula}`, { stdio: 'pipe' })
360
+ }
361
+ deleted.push('Homebrew: prjct-cli')
362
+ } catch (error) {
363
+ errors.push(`Homebrew: ${(error as Error).message}`)
364
+ }
365
+ }
366
+
367
+ if (installation.npm) {
368
+ try {
369
+ if (!options.dryRun) {
370
+ execSync('npm uninstall -g prjct-cli', { stdio: 'pipe' })
371
+ }
372
+ deleted.push('npm: prjct-cli')
373
+ } catch (error) {
374
+ errors.push(`npm: ${(error as Error).message}`)
375
+ }
376
+ }
377
+ }
378
+
379
+ return { deleted, errors }
380
+ }
381
+
382
+ /**
383
+ * Prompt user for confirmation
384
+ */
385
+ async function promptConfirmation(message: string): Promise<boolean> {
386
+ const rl = readline.createInterface({
387
+ input: process.stdin,
388
+ output: process.stdout,
389
+ })
390
+
391
+ return new Promise((resolve) => {
392
+ rl.question(message, (answer) => {
393
+ rl.close()
394
+ resolve(answer.toLowerCase() === 'uninstall')
395
+ })
396
+ })
397
+ }
398
+
399
+ /**
400
+ * Main uninstall function
401
+ */
402
+ export async function uninstall(
403
+ options: UninstallOptions = {},
404
+ _projectPath: string = process.cwd()
405
+ ): Promise<CommandResult> {
406
+ const items = await gatherUninstallItems()
407
+ const installation = detectInstallation()
408
+ const existingItems = items.filter((i) => i.exists)
409
+
410
+ // Check if there's anything to uninstall
411
+ if (existingItems.length === 0 && !installation.homebrew && !installation.npm) {
412
+ console.log(chalk.yellow('\nNo prjct installation found.'))
413
+ return {
414
+ success: true,
415
+ message: 'Nothing to uninstall',
416
+ }
417
+ }
418
+
419
+ // Calculate total size
420
+ const totalSize = existingItems.reduce((sum, item) => sum + (item.size || 0), 0)
421
+
422
+ // Display warning and items
423
+ console.log('')
424
+ console.log(chalk.red.bold(' WARNING: This action is DANGEROUS and IRREVERSIBLE'))
425
+ console.log('')
426
+ console.log(chalk.white('The following will be permanently deleted:'))
427
+ console.log('')
428
+
429
+ for (const item of existingItems) {
430
+ const displayPath = pathManager.getDisplayPath(item.path)
431
+ let info = ''
432
+
433
+ if (item.type === 'section') {
434
+ info = chalk.dim('(section only)')
435
+ } else if (item.size) {
436
+ info = chalk.dim(`(${formatSize(item.size)})`)
437
+ }
438
+
439
+ console.log(` ${chalk.cyan(displayPath.padEnd(35))} ${info}`)
440
+ console.log(` ${chalk.dim(item.description)}`)
441
+ console.log('')
442
+ }
443
+
444
+ // Show package manager installations
445
+ if (installation.homebrew) {
446
+ console.log(` ${chalk.cyan('Homebrew'.padEnd(35))} ${chalk.dim('prjct-cli formula')}`)
447
+ console.log('')
448
+ }
449
+
450
+ if (installation.npm) {
451
+ console.log(` ${chalk.cyan('npm global'.padEnd(35))} ${chalk.dim('prjct-cli package')}`)
452
+ console.log('')
453
+ }
454
+
455
+ if (totalSize > 0) {
456
+ console.log(chalk.dim(` Total size: ${formatSize(totalSize)}`))
457
+ console.log('')
458
+ }
459
+
460
+ // Handle dry run
461
+ if (options.dryRun) {
462
+ console.log(chalk.yellow('Dry run - no changes made'))
463
+ return {
464
+ success: true,
465
+ message: 'Dry run complete',
466
+ itemsFound: existingItems.length,
467
+ }
468
+ }
469
+
470
+ // Handle backup
471
+ if (options.backup) {
472
+ console.log(chalk.blue('Creating backup...'))
473
+ const backupPath = await createBackup()
474
+
475
+ if (backupPath) {
476
+ console.log(chalk.green(`Backup created: ${pathManager.getDisplayPath(backupPath)}`))
477
+ console.log('')
478
+ } else {
479
+ console.log(chalk.yellow('Failed to create backup, continuing...'))
480
+ }
481
+ }
482
+
483
+ // Require confirmation unless --force
484
+ if (!options.force) {
485
+ console.log(chalk.yellow('Type "uninstall" to confirm:'))
486
+ const confirmed = await promptConfirmation('> ')
487
+
488
+ if (!confirmed) {
489
+ console.log(chalk.yellow('\nUninstall cancelled.'))
490
+ return {
491
+ success: false,
492
+ message: 'Uninstall cancelled by user',
493
+ }
494
+ }
495
+ }
496
+
497
+ // Perform uninstallation
498
+ console.log('')
499
+ console.log(chalk.blue('Removing prjct...'))
500
+
501
+ const { deleted, errors } = await performUninstall(items, installation, options)
502
+
503
+ // Report results
504
+ console.log('')
505
+
506
+ if (deleted.length > 0) {
507
+ console.log(chalk.green(`Removed ${deleted.length} items`))
508
+ }
509
+
510
+ if (errors.length > 0) {
511
+ console.log(chalk.yellow(`\n${errors.length} errors:`))
512
+ for (const error of errors) {
513
+ console.log(chalk.red(` - ${error}`))
514
+ }
515
+ }
516
+
517
+ console.log('')
518
+ console.log(chalk.green('prjct has been uninstalled.'))
519
+ console.log(chalk.dim('Thanks for using prjct! We hope to see you again.'))
520
+ console.log('')
521
+
522
+ return {
523
+ success: errors.length === 0,
524
+ message: `Removed ${deleted.length} items`,
525
+ deleted,
526
+ errors: errors.length > 0 ? errors : undefined,
527
+ }
528
+ }
529
+
530
+ /**
531
+ * UninstallCommands class for integration with command system
532
+ */
533
+ export class UninstallCommands extends PrjctCommandsBase {
534
+ async uninstall(
535
+ options: UninstallOptions = {},
536
+ projectPath: string = process.cwd()
537
+ ): Promise<CommandResult> {
538
+ // Uninstall doesn't require project init
539
+ return uninstall(options, projectPath)
540
+ }
541
+ }