prjct-cli 0.15.0 → 0.17.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/CHANGELOG.md +51 -0
- package/bin/dev.js +0 -1
- package/bin/serve.js +19 -20
- package/core/agentic/agent-router.ts +79 -14
- package/core/agentic/command-executor/command-executor.ts +2 -74
- package/core/agentic/services.ts +0 -48
- package/core/agentic/template-loader.ts +35 -1
- package/core/commands/base.ts +96 -77
- package/core/commands/planning.ts +13 -2
- package/core/commands/setup.ts +3 -85
- package/core/errors.ts +209 -0
- package/core/infrastructure/config-manager.ts +22 -5
- package/core/infrastructure/setup.ts +5 -50
- package/core/storage/storage-manager.ts +42 -6
- package/core/utils/logger.ts +19 -12
- package/package.json +2 -4
- package/templates/agentic/subagent-generation.md +109 -0
- package/templates/commands/setup.md +18 -3
- package/templates/commands/sync.md +74 -13
- package/templates/mcp-config.json +20 -1
- package/templates/subagents/domain/backend.md +105 -0
- package/templates/subagents/domain/database.md +118 -0
- package/templates/subagents/domain/devops.md +148 -0
- package/templates/subagents/domain/frontend.md +99 -0
- package/templates/subagents/domain/testing.md +169 -0
- package/templates/subagents/workflow/prjct-planner.md +158 -0
- package/templates/subagents/workflow/prjct-shipper.md +179 -0
- package/templates/subagents/workflow/prjct-workflow.md +98 -0
- package/bin/generate-views.js +0 -209
- package/bin/migrate-to-json.js +0 -742
- package/core/agentic/context-filter.ts +0 -365
- package/core/agentic/parallel-tools.ts +0 -165
- package/core/agentic/response-templates.ts +0 -164
- package/core/agentic/semantic-compression.ts +0 -273
- package/core/agentic/think-blocks.ts +0 -202
- package/core/agentic/validation-rules.ts +0 -313
- package/core/domain/agent-matcher.ts +0 -130
- package/core/domain/agent-validator.ts +0 -250
- package/core/domain/architect-session.ts +0 -315
- package/core/domain/product-standards.ts +0 -106
- package/core/domain/smart-cache.ts +0 -167
- package/core/domain/task-analyzer.ts +0 -296
- package/core/infrastructure/legacy-installer-detector/cleanup.ts +0 -216
- package/core/infrastructure/legacy-installer-detector/detection.ts +0 -95
- package/core/infrastructure/legacy-installer-detector/index.ts +0 -171
- package/core/infrastructure/legacy-installer-detector/migration.ts +0 -87
- package/core/infrastructure/legacy-installer-detector/types.ts +0 -42
- package/core/infrastructure/legacy-installer-detector.ts +0 -7
- package/core/infrastructure/migrator/file-operations.ts +0 -125
- package/core/infrastructure/migrator/index.ts +0 -288
- package/core/infrastructure/migrator/project-scanner.ts +0 -90
- package/core/infrastructure/migrator/reports.ts +0 -117
- package/core/infrastructure/migrator/types.ts +0 -124
- package/core/infrastructure/migrator/validation.ts +0 -94
- package/core/infrastructure/migrator/version-migration.ts +0 -117
- package/core/infrastructure/migrator.ts +0 -10
- package/core/infrastructure/uuid-migration.ts +0 -750
- package/templates/commands/migrate-all.md +0 -96
- package/templates/commands/migrate.md +0 -140
|
@@ -1,296 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* TaskAnalyzer - Deep Semantic Task Analysis
|
|
3
|
-
*
|
|
4
|
-
* Analyzes tasks semantically, not just keywords
|
|
5
|
-
* Considers project context, history, and relationships
|
|
6
|
-
*
|
|
7
|
-
* @version 1.0.0
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import fs from 'fs/promises'
|
|
11
|
-
import path from 'path'
|
|
12
|
-
import analyzer from './analyzer'
|
|
13
|
-
import configManager from '../infrastructure/config-manager'
|
|
14
|
-
import pathManager from '../infrastructure/path-manager'
|
|
15
|
-
|
|
16
|
-
interface Task {
|
|
17
|
-
description?: string
|
|
18
|
-
type?: string
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
interface HistoryEntry {
|
|
22
|
-
type: string
|
|
23
|
-
domain?: string
|
|
24
|
-
description?: string
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
interface SemanticAnalysis {
|
|
28
|
-
intent: string | null
|
|
29
|
-
requiresMultipleAgents: boolean
|
|
30
|
-
text: string
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
interface HistoricalAnalysis {
|
|
34
|
-
confidence: number
|
|
35
|
-
patterns: Array<{ domain: string; description: string }>
|
|
36
|
-
suggestedDomain?: string | null
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
interface ProjectData {
|
|
40
|
-
packageJson: unknown
|
|
41
|
-
extensions: Record<string, number>
|
|
42
|
-
directories: string[]
|
|
43
|
-
configFiles: string[]
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
interface TaskAnalysisResult {
|
|
47
|
-
primaryDomain: string
|
|
48
|
-
confidence: number
|
|
49
|
-
semantic: SemanticAnalysis
|
|
50
|
-
historical: HistoricalAnalysis
|
|
51
|
-
complexity: string
|
|
52
|
-
projectData: ProjectData
|
|
53
|
-
matchedKeywords: string[]
|
|
54
|
-
reason: string
|
|
55
|
-
alternatives: string[]
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
class TaskAnalyzer {
|
|
59
|
-
projectPath: string
|
|
60
|
-
projectId: string | null = null
|
|
61
|
-
taskHistory: HistoryEntry[] | null = null
|
|
62
|
-
|
|
63
|
-
constructor(projectPath: string) {
|
|
64
|
-
this.projectPath = projectPath
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Initialize analyzer with project context
|
|
69
|
-
*/
|
|
70
|
-
async initialize(): Promise<void> {
|
|
71
|
-
this.projectId = await configManager.getProjectId(this.projectPath)
|
|
72
|
-
analyzer.init(this.projectPath)
|
|
73
|
-
await this.loadTaskHistory()
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Deep semantic analysis of a task
|
|
78
|
-
*
|
|
79
|
-
* 100% AGENTIC: No hardcoded patterns. Uses task description
|
|
80
|
-
* and historical patterns only. Claude decides domain relevance.
|
|
81
|
-
*/
|
|
82
|
-
async analyzeTask(task: Task): Promise<TaskAnalysisResult> {
|
|
83
|
-
if (!this.projectId) {
|
|
84
|
-
await this.initialize()
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const description = (task.description || '').toLowerCase()
|
|
88
|
-
const type = (task.type || '').toLowerCase()
|
|
89
|
-
const fullText = `${description} ${type}`.trim()
|
|
90
|
-
|
|
91
|
-
// Get raw project data (no categorization)
|
|
92
|
-
const projectData: ProjectData = {
|
|
93
|
-
packageJson: await analyzer.readPackageJson(),
|
|
94
|
-
extensions: await analyzer.getFileExtensions(),
|
|
95
|
-
directories: await analyzer.listDirectories(),
|
|
96
|
-
configFiles: await analyzer.listConfigFiles(),
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// Semantic understanding (intent-based, not keyword-based)
|
|
100
|
-
const semantic = this.analyzeSemantics(fullText)
|
|
101
|
-
|
|
102
|
-
// Historical patterns
|
|
103
|
-
const historical = await this.analyzeHistory(fullText)
|
|
104
|
-
|
|
105
|
-
// Complexity estimation
|
|
106
|
-
const complexity = this.estimateComplexity(fullText)
|
|
107
|
-
|
|
108
|
-
// Primary domain from history and intent (not hardcoded patterns)
|
|
109
|
-
const primaryDomain = this.selectPrimaryDomain(semantic, historical)
|
|
110
|
-
const confidence = this.calculateConfidence(semantic, historical)
|
|
111
|
-
|
|
112
|
-
return {
|
|
113
|
-
primaryDomain,
|
|
114
|
-
confidence,
|
|
115
|
-
semantic,
|
|
116
|
-
historical,
|
|
117
|
-
complexity,
|
|
118
|
-
projectData, // Raw data for Claude to analyze
|
|
119
|
-
matchedKeywords: [], // No keyword matching - Claude decides
|
|
120
|
-
reason: this.buildReason(primaryDomain, semantic, historical),
|
|
121
|
-
alternatives: ['full-stack', 'generalist'],
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* Domain detection removed - 100% AGENTIC
|
|
127
|
-
*
|
|
128
|
-
* NO hardcoded keyword lists or framework categorization.
|
|
129
|
-
* Claude analyzes the task description and project context
|
|
130
|
-
* to determine the appropriate domain.
|
|
131
|
-
*
|
|
132
|
-
* This method is kept for backward compatibility but returns empty.
|
|
133
|
-
* Use analyzeTask() which provides raw data for Claude.
|
|
134
|
-
*/
|
|
135
|
-
detectDomains(text: string): Record<string, unknown> {
|
|
136
|
-
// No hardcoded patterns - Claude decides domain
|
|
137
|
-
return {}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Semantic analysis - understand intent
|
|
142
|
-
* AGENTIC: Claude uses templates/analysis/intent.md for detailed analysis
|
|
143
|
-
* This returns minimal structure - Claude determines actual intent
|
|
144
|
-
*/
|
|
145
|
-
analyzeSemantics(text: string): SemanticAnalysis {
|
|
146
|
-
// AGENTIC: Return structure only, Claude analyzes via template
|
|
147
|
-
return {
|
|
148
|
-
intent: null, // Claude determines via templates/analysis/intent.md
|
|
149
|
-
requiresMultipleAgents: false, // Claude decides based on context
|
|
150
|
-
text: text, // Pass text for Claude to analyze
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* Analyze historical patterns
|
|
156
|
-
*/
|
|
157
|
-
async analyzeHistory(text: string): Promise<HistoricalAnalysis> {
|
|
158
|
-
if (!this.taskHistory) {
|
|
159
|
-
return { confidence: 0, patterns: [] }
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// Find similar tasks in history
|
|
163
|
-
const similar = this.taskHistory.filter((task) => {
|
|
164
|
-
const similarity = this.calculateTextSimilarity(text, task.description || '')
|
|
165
|
-
return similarity > 0.5
|
|
166
|
-
})
|
|
167
|
-
|
|
168
|
-
if (similar.length === 0) {
|
|
169
|
-
return { confidence: 0, patterns: [] }
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// Find most common domain for similar tasks
|
|
173
|
-
const domainCounts: Record<string, number> = {}
|
|
174
|
-
similar.forEach((task) => {
|
|
175
|
-
if (task.domain) {
|
|
176
|
-
domainCounts[task.domain] = (domainCounts[task.domain] || 0) + 1
|
|
177
|
-
}
|
|
178
|
-
})
|
|
179
|
-
|
|
180
|
-
const mostCommon = Object.entries(domainCounts).sort((a, b) => b[1] - a[1])[0]
|
|
181
|
-
|
|
182
|
-
return {
|
|
183
|
-
confidence: mostCommon ? mostCommon[1] / similar.length : 0,
|
|
184
|
-
patterns: similar.map((t) => ({ domain: t.domain || '', description: t.description || '' })),
|
|
185
|
-
suggestedDomain: mostCommon ? mostCommon[0] : null,
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/**
|
|
190
|
-
* Estimate task complexity
|
|
191
|
-
* AGENTIC: Claude uses templates/analysis/complexity.md for real estimation
|
|
192
|
-
* This returns default - Claude determines actual complexity
|
|
193
|
-
*/
|
|
194
|
-
estimateComplexity(text: string): string {
|
|
195
|
-
// AGENTIC: Return default, Claude analyzes via templates/analysis/complexity.md
|
|
196
|
-
return 'medium'
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* Select primary domain from history and semantics
|
|
201
|
-
*
|
|
202
|
-
* 100% AGENTIC: No keyword matching. Uses only:
|
|
203
|
-
* - Historical patterns from past tasks
|
|
204
|
-
* - Basic intent detection
|
|
205
|
-
* Claude decides actual domain based on project context.
|
|
206
|
-
*/
|
|
207
|
-
selectPrimaryDomain(semantic: SemanticAnalysis, historical: HistoricalAnalysis): string {
|
|
208
|
-
// Priority: historical > default
|
|
209
|
-
if (historical && historical.suggestedDomain && historical.confidence > 0.7) {
|
|
210
|
-
return historical.suggestedDomain
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// Map intent to suggested domain (loose mapping, Claude refines)
|
|
214
|
-
if (semantic && semantic.intent === 'test') {
|
|
215
|
-
return 'qa'
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
// Default: generalist (Claude decides based on context)
|
|
219
|
-
return 'generalist'
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
/**
|
|
223
|
-
* Calculate confidence based on available signals
|
|
224
|
-
*/
|
|
225
|
-
calculateConfidence(semantic: SemanticAnalysis, historical: HistoricalAnalysis): number {
|
|
226
|
-
let confidence = 0.5 // Base confidence
|
|
227
|
-
|
|
228
|
-
// Boost from historical patterns
|
|
229
|
-
if (historical && historical.confidence > 0) {
|
|
230
|
-
confidence += historical.confidence * 0.3
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// Boost from semantic understanding
|
|
234
|
-
if (semantic && semantic.intent) {
|
|
235
|
-
confidence += 0.1
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
return Math.min(confidence, 1.0)
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
/**
|
|
242
|
-
* Build human-readable reason
|
|
243
|
-
*/
|
|
244
|
-
buildReason(primaryDomain: string, semantic: SemanticAnalysis, historical: HistoricalAnalysis): string {
|
|
245
|
-
const parts: string[] = []
|
|
246
|
-
|
|
247
|
-
if (historical && historical.suggestedDomain && historical.confidence > 0.7) {
|
|
248
|
-
parts.push(`Historical: similar tasks used ${primaryDomain}`)
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
if (semantic && semantic.intent) {
|
|
252
|
-
parts.push(`Intent: ${semantic.intent}`)
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
return parts.join(' | ') || 'Claude will analyze task in context'
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
/**
|
|
259
|
-
* Load task history from memory
|
|
260
|
-
*/
|
|
261
|
-
async loadTaskHistory(): Promise<void> {
|
|
262
|
-
try {
|
|
263
|
-
const memoryPath = pathManager.getFilePath(this.projectId!, 'memory', 'context.jsonl')
|
|
264
|
-
const content = await fs.readFile(memoryPath, 'utf-8')
|
|
265
|
-
const lines = content.split('\n').filter(Boolean)
|
|
266
|
-
|
|
267
|
-
this.taskHistory = lines
|
|
268
|
-
.map((line) => {
|
|
269
|
-
try {
|
|
270
|
-
return JSON.parse(line) as HistoryEntry
|
|
271
|
-
} catch {
|
|
272
|
-
return null
|
|
273
|
-
}
|
|
274
|
-
})
|
|
275
|
-
.filter((entry): entry is HistoryEntry => entry !== null && entry.type === 'task_start' && !!entry.domain)
|
|
276
|
-
.slice(-100) // Last 100 tasks
|
|
277
|
-
} catch {
|
|
278
|
-
this.taskHistory = []
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
/**
|
|
283
|
-
* Calculate text similarity (simple Jaccard)
|
|
284
|
-
*/
|
|
285
|
-
calculateTextSimilarity(text1: string, text2: string): number {
|
|
286
|
-
const words1 = new Set(text1.toLowerCase().split(/\s+/))
|
|
287
|
-
const words2 = new Set(text2.toLowerCase().split(/\s+/))
|
|
288
|
-
|
|
289
|
-
const intersection = new Set([...words1].filter((w) => words2.has(w)))
|
|
290
|
-
const union = new Set([...words1, ...words2])
|
|
291
|
-
|
|
292
|
-
return intersection.size / union.size
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
export default TaskAnalyzer
|
|
@@ -1,216 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Cleanup - Legacy Installation Cleanup
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import fs from 'fs/promises'
|
|
6
|
-
import path from 'path'
|
|
7
|
-
import os from 'os'
|
|
8
|
-
import { legacyInstallDir, isWindows } from './detection'
|
|
9
|
-
import type { CleanupResult } from './types'
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Get platform-specific shell config files
|
|
13
|
-
*/
|
|
14
|
-
export function getShellConfigFiles(): string[] {
|
|
15
|
-
if (isWindows) {
|
|
16
|
-
const profilePaths: string[] = []
|
|
17
|
-
|
|
18
|
-
if (process.env.USERPROFILE) {
|
|
19
|
-
profilePaths.push(
|
|
20
|
-
path.join(
|
|
21
|
-
process.env.USERPROFILE,
|
|
22
|
-
'Documents',
|
|
23
|
-
'PowerShell',
|
|
24
|
-
'Microsoft.PowerShell_profile.ps1'
|
|
25
|
-
),
|
|
26
|
-
path.join(
|
|
27
|
-
process.env.USERPROFILE,
|
|
28
|
-
'Documents',
|
|
29
|
-
'WindowsPowerShell',
|
|
30
|
-
'Microsoft.PowerShell_profile.ps1'
|
|
31
|
-
)
|
|
32
|
-
)
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
return profilePaths
|
|
36
|
-
} else {
|
|
37
|
-
return [
|
|
38
|
-
path.join(os.homedir(), '.zshrc'),
|
|
39
|
-
path.join(os.homedir(), '.bashrc'),
|
|
40
|
-
path.join(os.homedir(), '.profile'),
|
|
41
|
-
path.join(os.homedir(), '.bash_profile'),
|
|
42
|
-
]
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Remove legacy installation files (keep projects data)
|
|
48
|
-
*/
|
|
49
|
-
export async function cleanupLegacyInstallation(): Promise<CleanupResult> {
|
|
50
|
-
const result: CleanupResult = {
|
|
51
|
-
success: false,
|
|
52
|
-
message: '',
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
try {
|
|
56
|
-
const dirsToRemove = [
|
|
57
|
-
'bin',
|
|
58
|
-
'core',
|
|
59
|
-
'templates',
|
|
60
|
-
'scripts',
|
|
61
|
-
'node_modules',
|
|
62
|
-
'.git',
|
|
63
|
-
'__tests__',
|
|
64
|
-
'website',
|
|
65
|
-
'docs',
|
|
66
|
-
'.github',
|
|
67
|
-
]
|
|
68
|
-
const filesToRemove = [
|
|
69
|
-
'package.json',
|
|
70
|
-
'package-lock.json',
|
|
71
|
-
'README.md',
|
|
72
|
-
'LICENSE',
|
|
73
|
-
'CHANGELOG.md',
|
|
74
|
-
'CLAUDE.md',
|
|
75
|
-
'CONTRIBUTING.md',
|
|
76
|
-
'MIGRATION.md',
|
|
77
|
-
'TESTING.md',
|
|
78
|
-
'.gitignore',
|
|
79
|
-
'.eslintrc.js',
|
|
80
|
-
'.prettierrc',
|
|
81
|
-
'vitest.config.js',
|
|
82
|
-
'vitest.workspace.js',
|
|
83
|
-
]
|
|
84
|
-
|
|
85
|
-
let removedItems = 0
|
|
86
|
-
|
|
87
|
-
for (const dir of dirsToRemove) {
|
|
88
|
-
const dirPath = path.join(legacyInstallDir, dir)
|
|
89
|
-
try {
|
|
90
|
-
await fs.rm(dirPath, { recursive: true, force: true })
|
|
91
|
-
removedItems++
|
|
92
|
-
} catch {
|
|
93
|
-
// Directory doesn't exist
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
for (const file of filesToRemove) {
|
|
98
|
-
const filePath = path.join(legacyInstallDir, file)
|
|
99
|
-
try {
|
|
100
|
-
await fs.unlink(filePath)
|
|
101
|
-
removedItems++
|
|
102
|
-
} catch {
|
|
103
|
-
// File doesn't exist
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
result.success = true
|
|
108
|
-
result.message = `Removed ${removedItems} legacy installation items`
|
|
109
|
-
return result
|
|
110
|
-
} catch (error) {
|
|
111
|
-
result.message = `Cleanup failed: ${(error as Error).message}`
|
|
112
|
-
return result
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Clean up legacy PATH entries from shell config files
|
|
118
|
-
*/
|
|
119
|
-
export async function cleanupLegacyPATH(): Promise<CleanupResult> {
|
|
120
|
-
const result: CleanupResult = {
|
|
121
|
-
success: false,
|
|
122
|
-
message: '',
|
|
123
|
-
filesModified: 0,
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
try {
|
|
127
|
-
const shellConfigs = getShellConfigFiles()
|
|
128
|
-
|
|
129
|
-
for (const configFile of shellConfigs) {
|
|
130
|
-
try {
|
|
131
|
-
const content = await fs.readFile(configFile, 'utf8')
|
|
132
|
-
|
|
133
|
-
if (!content.includes('.prjct-cli/bin')) {
|
|
134
|
-
continue
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const lines = content.split('\n')
|
|
138
|
-
const filteredLines = lines.filter((line) => {
|
|
139
|
-
return !line.includes('.prjct-cli/bin') && !line.includes('# prjct/cli')
|
|
140
|
-
})
|
|
141
|
-
|
|
142
|
-
const cleanedLines: string[] = []
|
|
143
|
-
for (let i = 0; i < filteredLines.length; i++) {
|
|
144
|
-
const line = filteredLines[i]
|
|
145
|
-
const prevLine = filteredLines[i - 1]
|
|
146
|
-
|
|
147
|
-
if (line.trim() === '' && prevLine && prevLine.trim() === '') {
|
|
148
|
-
continue
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
cleanedLines.push(line)
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
await fs.writeFile(configFile, cleanedLines.join('\n'), 'utf8')
|
|
155
|
-
result.filesModified!++
|
|
156
|
-
} catch {
|
|
157
|
-
// File doesn't exist or can't read
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
result.success = true
|
|
162
|
-
result.message =
|
|
163
|
-
result.filesModified! > 0
|
|
164
|
-
? `Cleaned PATH from ${result.filesModified} shell config(s)`
|
|
165
|
-
: 'No legacy PATH entries found'
|
|
166
|
-
|
|
167
|
-
return result
|
|
168
|
-
} catch (error) {
|
|
169
|
-
result.message = `PATH cleanup failed: ${(error as Error).message}`
|
|
170
|
-
return result
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* Clean up legacy symlinks
|
|
176
|
-
*/
|
|
177
|
-
export async function cleanupLegacySymlinks(): Promise<CleanupResult> {
|
|
178
|
-
const result: CleanupResult = {
|
|
179
|
-
success: false,
|
|
180
|
-
message: '',
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
if (isWindows) {
|
|
184
|
-
result.success = true
|
|
185
|
-
result.message = 'No symlinks on Windows'
|
|
186
|
-
return result
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
try {
|
|
190
|
-
const symlinkPath = path.join(os.homedir(), '.local', 'bin', 'prjct')
|
|
191
|
-
|
|
192
|
-
try {
|
|
193
|
-
const stat = await fs.lstat(symlinkPath)
|
|
194
|
-
|
|
195
|
-
if (stat.isSymbolicLink()) {
|
|
196
|
-
const target = await fs.readlink(symlinkPath)
|
|
197
|
-
|
|
198
|
-
if (target.includes('.prjct-cli')) {
|
|
199
|
-
await fs.unlink(symlinkPath)
|
|
200
|
-
result.success = true
|
|
201
|
-
result.message = 'Removed legacy symlink'
|
|
202
|
-
return result
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
} catch {
|
|
206
|
-
// Symlink doesn't exist
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
result.success = true
|
|
210
|
-
result.message = 'No legacy symlinks found'
|
|
211
|
-
return result
|
|
212
|
-
} catch (error) {
|
|
213
|
-
result.message = `Symlink cleanup failed: ${(error as Error).message}`
|
|
214
|
-
return result
|
|
215
|
-
}
|
|
216
|
-
}
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Detection - Legacy Installation Detection
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import fs from 'fs/promises'
|
|
6
|
-
import path from 'path'
|
|
7
|
-
import os from 'os'
|
|
8
|
-
import { execSync } from 'child_process'
|
|
9
|
-
|
|
10
|
-
export const legacyInstallDir = path.join(os.homedir(), '.prjct-cli')
|
|
11
|
-
export const npmGlobalProjectsDir = path.join(os.homedir(), '.prjct-cli', 'projects')
|
|
12
|
-
export const isWindows = process.platform === 'win32'
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Check if legacy curl installation exists
|
|
16
|
-
*/
|
|
17
|
-
export async function hasLegacyInstallation(): Promise<boolean> {
|
|
18
|
-
try {
|
|
19
|
-
const stat = await fs.stat(legacyInstallDir)
|
|
20
|
-
if (!stat.isDirectory()) return false
|
|
21
|
-
|
|
22
|
-
// Check for .git directory (indicates curl install)
|
|
23
|
-
try {
|
|
24
|
-
await fs.access(path.join(legacyInstallDir, '.git'))
|
|
25
|
-
return true
|
|
26
|
-
} catch {
|
|
27
|
-
// No .git, check for other legacy indicators
|
|
28
|
-
const entries = await fs.readdir(legacyInstallDir)
|
|
29
|
-
|
|
30
|
-
// Legacy has: bin/, core/, templates/, scripts/, package.json
|
|
31
|
-
const legacyFiles = ['bin', 'core', 'templates', 'scripts', 'package.json']
|
|
32
|
-
const hasLegacyFiles = legacyFiles.every((file) => entries.includes(file))
|
|
33
|
-
|
|
34
|
-
if (hasLegacyFiles) {
|
|
35
|
-
return true
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// Only has projects/ and config/ = already migrated
|
|
39
|
-
const onlyDataDirs = entries.every((entry) => ['projects', 'config'].includes(entry))
|
|
40
|
-
return !onlyDataDirs
|
|
41
|
-
}
|
|
42
|
-
} catch {
|
|
43
|
-
return false
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Get npm global installation path
|
|
49
|
-
*/
|
|
50
|
-
export function getNpmGlobalPath(): string {
|
|
51
|
-
try {
|
|
52
|
-
const npmRoot = execSync('npm root -g', { encoding: 'utf8' }).trim()
|
|
53
|
-
return path.join(npmRoot, 'prjct-cli')
|
|
54
|
-
} catch {
|
|
55
|
-
// Fallback to common locations
|
|
56
|
-
const nodePath = process.execPath
|
|
57
|
-
const nodeDir = path.dirname(path.dirname(nodePath))
|
|
58
|
-
return path.join(nodeDir, 'lib', 'node_modules', 'prjct-cli')
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Check if user has npm global installation
|
|
64
|
-
*/
|
|
65
|
-
export async function hasNpmInstallation(): Promise<boolean> {
|
|
66
|
-
try {
|
|
67
|
-
execSync('npm list -g prjct-cli', { stdio: 'ignore' })
|
|
68
|
-
return true
|
|
69
|
-
} catch {
|
|
70
|
-
return false
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Get version of legacy installation
|
|
76
|
-
*/
|
|
77
|
-
export async function getLegacyVersion(): Promise<string | null> {
|
|
78
|
-
try {
|
|
79
|
-
const packageJsonPath = path.join(legacyInstallDir, 'package.json')
|
|
80
|
-
const content = await fs.readFile(packageJsonPath, 'utf8')
|
|
81
|
-
const pkg = JSON.parse(content)
|
|
82
|
-
return pkg.version || 'unknown'
|
|
83
|
-
} catch {
|
|
84
|
-
return null
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Quick check - silent, returns true if cleanup needed
|
|
90
|
-
*/
|
|
91
|
-
export async function needsCleanup(): Promise<boolean> {
|
|
92
|
-
const hasLegacy = await hasLegacyInstallation()
|
|
93
|
-
const hasNpm = await hasNpmInstallation()
|
|
94
|
-
return hasLegacy && hasNpm
|
|
95
|
-
}
|