prjct-cli 0.15.1 → 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 +35 -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/sync.md +74 -13
- 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,315 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Architect Session Manager
|
|
3
|
-
* Handles conversational state for ARCHITECT MODE (Agent-based, not deterministic)
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import fs from 'fs/promises'
|
|
7
|
-
import path from 'path'
|
|
8
|
-
|
|
9
|
-
interface ConversationEntry {
|
|
10
|
-
question: string
|
|
11
|
-
answer: string
|
|
12
|
-
timestamp: string
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
interface ArchitectSessionData {
|
|
16
|
-
idea: string
|
|
17
|
-
projectType: string
|
|
18
|
-
active: boolean
|
|
19
|
-
startedAt: string
|
|
20
|
-
completedAt?: string
|
|
21
|
-
conversation: ConversationEntry[]
|
|
22
|
-
answers: Record<string, string>
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
interface SessionSummary {
|
|
26
|
-
idea: string
|
|
27
|
-
projectType: string
|
|
28
|
-
conversationLength: number
|
|
29
|
-
insights: number
|
|
30
|
-
startedAt: string
|
|
31
|
-
completedAt: string
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
class ArchitectSession {
|
|
35
|
-
/**
|
|
36
|
-
* Initialize new architect session
|
|
37
|
-
*/
|
|
38
|
-
async init(idea: string, projectType: string, globalPath: string): Promise<ArchitectSessionData> {
|
|
39
|
-
const session: ArchitectSessionData = {
|
|
40
|
-
idea,
|
|
41
|
-
projectType,
|
|
42
|
-
active: true,
|
|
43
|
-
startedAt: new Date().toISOString(),
|
|
44
|
-
conversation: [],
|
|
45
|
-
answers: {},
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
await this.save(session, globalPath)
|
|
49
|
-
return session
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Load active session
|
|
54
|
-
*/
|
|
55
|
-
async load(globalPath: string): Promise<ArchitectSessionData | null> {
|
|
56
|
-
try {
|
|
57
|
-
const sessionPath = path.join(globalPath, 'planning', 'architect-session.json')
|
|
58
|
-
const content = await fs.readFile(sessionPath, 'utf-8')
|
|
59
|
-
return JSON.parse(content)
|
|
60
|
-
} catch {
|
|
61
|
-
return null
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Save session to disk
|
|
67
|
-
*/
|
|
68
|
-
async save(session: ArchitectSessionData, globalPath: string): Promise<void> {
|
|
69
|
-
const planningDir = path.join(globalPath, 'planning')
|
|
70
|
-
await fs.mkdir(planningDir, { recursive: true })
|
|
71
|
-
|
|
72
|
-
const sessionPath = path.join(planningDir, 'architect-session.json')
|
|
73
|
-
await fs.writeFile(sessionPath, JSON.stringify(session, null, 2))
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Log a Q&A pair to the conversation
|
|
78
|
-
*/
|
|
79
|
-
async logQA(question: string, answer: string, globalPath: string): Promise<void> {
|
|
80
|
-
const session = await this.load(globalPath)
|
|
81
|
-
|
|
82
|
-
if (!session || !session.active) {
|
|
83
|
-
throw new Error('No active architect session')
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
session.conversation.push({
|
|
87
|
-
question,
|
|
88
|
-
answer,
|
|
89
|
-
timestamp: new Date().toISOString(),
|
|
90
|
-
})
|
|
91
|
-
|
|
92
|
-
await this.save(session, globalPath)
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Save key insight for plan generation
|
|
97
|
-
*/
|
|
98
|
-
async saveInsight(key: string, value: string, globalPath: string): Promise<void> {
|
|
99
|
-
const session = await this.load(globalPath)
|
|
100
|
-
|
|
101
|
-
if (!session || !session.active) {
|
|
102
|
-
throw new Error('No active architect session')
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
session.answers[key] = value
|
|
106
|
-
await this.save(session, globalPath)
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Complete session and generate plan
|
|
111
|
-
*/
|
|
112
|
-
async complete(globalPath: string): Promise<SessionSummary> {
|
|
113
|
-
const session = await this.load(globalPath)
|
|
114
|
-
|
|
115
|
-
if (!session || !session.active) {
|
|
116
|
-
throw new Error('No active architect session')
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// Generate plan MD
|
|
120
|
-
await this.generatePlan(session, globalPath)
|
|
121
|
-
|
|
122
|
-
// Mark session as complete
|
|
123
|
-
session.active = false
|
|
124
|
-
session.completedAt = new Date().toISOString()
|
|
125
|
-
await this.save(session, globalPath)
|
|
126
|
-
|
|
127
|
-
return this.buildSummary(session)
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* Generate architect plan MD file
|
|
132
|
-
*/
|
|
133
|
-
async generatePlan(session: ArchitectSessionData, globalPath: string): Promise<void> {
|
|
134
|
-
const plan = this.buildPlanMarkdown(session)
|
|
135
|
-
|
|
136
|
-
const planPath = path.join(globalPath, 'planning', 'architect-session.md')
|
|
137
|
-
await fs.writeFile(planPath, plan)
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Build plan markdown content
|
|
142
|
-
*/
|
|
143
|
-
buildPlanMarkdown(session: ArchitectSessionData): string {
|
|
144
|
-
const { projectType, idea, conversation, answers } = session
|
|
145
|
-
|
|
146
|
-
// Build conversation log
|
|
147
|
-
const conversationLog = conversation
|
|
148
|
-
.map((qa, i) => `### Q${i + 1}: ${qa.question}\n**A**: ${qa.answer}\n\n_${qa.timestamp}_`)
|
|
149
|
-
.join('\n\n')
|
|
150
|
-
|
|
151
|
-
// Build stack summary from answers
|
|
152
|
-
const stackSummary = this.buildStackSummary(answers)
|
|
153
|
-
|
|
154
|
-
// Build Context7 queries
|
|
155
|
-
const context7Queries = this.buildContext7Queries(answers)
|
|
156
|
-
|
|
157
|
-
// Build implementation steps
|
|
158
|
-
const steps = this.buildImplementationSteps(session)
|
|
159
|
-
|
|
160
|
-
return `# ARCHITECT SESSION: ${idea}
|
|
161
|
-
|
|
162
|
-
## Project Idea
|
|
163
|
-
${idea}
|
|
164
|
-
|
|
165
|
-
## Project Type
|
|
166
|
-
${projectType}
|
|
167
|
-
|
|
168
|
-
## Discovery Conversation
|
|
169
|
-
|
|
170
|
-
${conversationLog}
|
|
171
|
-
|
|
172
|
-
## Architecture Summary
|
|
173
|
-
|
|
174
|
-
**Stack:**
|
|
175
|
-
${stackSummary}
|
|
176
|
-
|
|
177
|
-
## Implementation Plan
|
|
178
|
-
|
|
179
|
-
**Context7 Queries:**
|
|
180
|
-
${context7Queries.map((q) => `- "${q}"`).join('\n')}
|
|
181
|
-
|
|
182
|
-
**Implementation Steps:**
|
|
183
|
-
${steps.map((step, i) => `${i + 1}. ${step}`).join('\n')}
|
|
184
|
-
|
|
185
|
-
## Execution
|
|
186
|
-
|
|
187
|
-
This plan is ready to be executed.
|
|
188
|
-
|
|
189
|
-
**To generate code:**
|
|
190
|
-
\`\`\`
|
|
191
|
-
/p:architect execute
|
|
192
|
-
\`\`\`
|
|
193
|
-
|
|
194
|
-
The agent will:
|
|
195
|
-
1. Read this architectural plan
|
|
196
|
-
2. Use Context7 to fetch official documentation
|
|
197
|
-
3. Generate project structure following best practices
|
|
198
|
-
4. Create starter files with boilerplate code
|
|
199
|
-
|
|
200
|
-
---
|
|
201
|
-
Generated: ${new Date().toISOString()}
|
|
202
|
-
`
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* Build stack summary from answers
|
|
207
|
-
*/
|
|
208
|
-
buildStackSummary(answers: Record<string, string>): string {
|
|
209
|
-
const parts: string[] = []
|
|
210
|
-
|
|
211
|
-
for (const [key, value] of Object.entries(answers)) {
|
|
212
|
-
if (value && value !== 'Ninguna' && value !== 'None' && value !== 'Otro') {
|
|
213
|
-
parts.push(`- **${key}**: ${value}`)
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
return parts.length > 0 ? parts.join('\n') : '- To be determined during implementation'
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
/**
|
|
221
|
-
* Build Context7 queries from answers
|
|
222
|
-
*/
|
|
223
|
-
buildContext7Queries(answers: Record<string, string>): string[] {
|
|
224
|
-
const queries: string[] = []
|
|
225
|
-
|
|
226
|
-
if (answers.framework) {
|
|
227
|
-
queries.push(`${answers.framework} getting started`)
|
|
228
|
-
queries.push(`${answers.framework} project structure`)
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
if (answers.language && answers.framework) {
|
|
232
|
-
queries.push(`${answers.language} ${answers.framework} best practices`)
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
if (answers.database && answers.language) {
|
|
236
|
-
queries.push(`${answers.database} ${answers.language} integration`)
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
if (answers.auth) {
|
|
240
|
-
queries.push(`${answers.auth} implementation ${answers.language || ''}`.trim())
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
// Always include general queries if specific ones not available
|
|
244
|
-
if (queries.length === 0 && answers.language) {
|
|
245
|
-
queries.push(`${answers.language} project structure`)
|
|
246
|
-
queries.push(`${answers.language} best practices`)
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
return queries
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
/**
|
|
253
|
-
* Build implementation steps from session
|
|
254
|
-
*/
|
|
255
|
-
buildImplementationSteps(session: ArchitectSessionData): string[] {
|
|
256
|
-
const { answers } = session
|
|
257
|
-
const steps: string[] = []
|
|
258
|
-
|
|
259
|
-
// Generic steps - Claude will refine during execution
|
|
260
|
-
if (answers.language) {
|
|
261
|
-
steps.push(`Initialize ${answers.language} project`)
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
if (answers.framework) {
|
|
265
|
-
steps.push(`Setup ${answers.framework}`)
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
if (answers.database) {
|
|
269
|
-
steps.push(`Configure ${answers.database}`)
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
if (answers.auth) {
|
|
273
|
-
steps.push(`Implement ${answers.auth}`)
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
steps.push('Create project structure')
|
|
277
|
-
steps.push('Generate starter files')
|
|
278
|
-
|
|
279
|
-
if (answers.deployment) {
|
|
280
|
-
steps.push(`Setup ${answers.deployment} configuration`)
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
return steps
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
/**
|
|
287
|
-
* Build summary of session
|
|
288
|
-
*/
|
|
289
|
-
buildSummary(session: ArchitectSessionData): SessionSummary {
|
|
290
|
-
return {
|
|
291
|
-
idea: session.idea,
|
|
292
|
-
projectType: session.projectType,
|
|
293
|
-
conversationLength: session.conversation.length,
|
|
294
|
-
insights: Object.keys(session.answers).length,
|
|
295
|
-
startedAt: session.startedAt,
|
|
296
|
-
completedAt: session.completedAt || new Date().toISOString(),
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
/**
|
|
301
|
-
* Clear architect session
|
|
302
|
-
*/
|
|
303
|
-
async clear(globalPath: string): Promise<void> {
|
|
304
|
-
try {
|
|
305
|
-
const sessionPath = path.join(globalPath, 'planning', 'architect-session.json')
|
|
306
|
-
await fs.unlink(sessionPath)
|
|
307
|
-
} catch {
|
|
308
|
-
// Ignore if doesn't exist
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
const architectSession = new ArchitectSession()
|
|
314
|
-
export default architectSession
|
|
315
|
-
export { ArchitectSession }
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Modern Product Standards
|
|
3
|
-
* Defines what "Good" looks like for each domain.
|
|
4
|
-
* Used to inject high standards into agent prompts.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
interface DomainStandard {
|
|
8
|
-
title: string
|
|
9
|
-
rules: string[]
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
interface Standards {
|
|
13
|
-
title: string
|
|
14
|
-
rules: string[]
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
interface ProductStandardsType {
|
|
18
|
-
general: string[]
|
|
19
|
-
domains: Record<string, DomainStandard>
|
|
20
|
-
getStandards(domain?: string): Standards
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const ProductStandards: ProductStandardsType = {
|
|
24
|
-
// General standards applicable to all agents
|
|
25
|
-
general: [
|
|
26
|
-
'SHIP IT: Bias for action. Better to ship and iterate than perfect and delay.',
|
|
27
|
-
'USER CENTRIC: Always ask "How does this help the user?"',
|
|
28
|
-
'CLEAN CODE: Write code that is easy to read, test, and maintain.',
|
|
29
|
-
'NO BS: Avoid over-engineering. Simple is better than complex.',
|
|
30
|
-
],
|
|
31
|
-
|
|
32
|
-
// Domain-specific standards
|
|
33
|
-
domains: {
|
|
34
|
-
frontend: {
|
|
35
|
-
title: 'Modern Frontend Standards',
|
|
36
|
-
rules: [
|
|
37
|
-
'PERFORMANCE: Core Web Vitals matter. Optimize LCP, CLS, FID.',
|
|
38
|
-
'ACCESSIBILITY: Semantic HTML, ARIA labels, keyboard navigation (WCAG 2.1 AA).',
|
|
39
|
-
'RESPONSIVE: Mobile-first design. Works on all devices.',
|
|
40
|
-
'UX/UI: Smooth transitions, loading states, error boundaries. No dead clicks.',
|
|
41
|
-
'STATE: Local state for UI, Global state (Context/Zustand) for data. No prop drilling.',
|
|
42
|
-
],
|
|
43
|
-
},
|
|
44
|
-
backend: {
|
|
45
|
-
title: 'Robust Backend Standards',
|
|
46
|
-
rules: [
|
|
47
|
-
'SECURITY: Validate ALL inputs. Sanitize outputs. OWASP Top 10 awareness.',
|
|
48
|
-
'SCALABILITY: Stateless services. Caching strategies (Redis/CDN).',
|
|
49
|
-
'RELIABILITY: Graceful error handling. Structured logging. Health checks.',
|
|
50
|
-
'API DESIGN: RESTful or GraphQL best practices. Consistent response envelopes.',
|
|
51
|
-
'DB: Indexed queries. Migrations for schema changes. No N+1 queries.',
|
|
52
|
-
],
|
|
53
|
-
},
|
|
54
|
-
database: {
|
|
55
|
-
title: 'Data Integrity Standards',
|
|
56
|
-
rules: [
|
|
57
|
-
'INTEGRITY: Foreign keys, constraints, transactions.',
|
|
58
|
-
'PERFORMANCE: Index usage analysis. Query optimization.',
|
|
59
|
-
'BACKUPS: Point-in-time recovery awareness.',
|
|
60
|
-
'MIGRATIONS: Idempotent scripts. Zero-downtime changes.',
|
|
61
|
-
],
|
|
62
|
-
},
|
|
63
|
-
devops: {
|
|
64
|
-
title: 'Modern Ops Standards',
|
|
65
|
-
rules: [
|
|
66
|
-
'AUTOMATION: CI/CD for everything. No manual deployments.',
|
|
67
|
-
'IaC: Infrastructure as Code (Terraform/Pulumi).',
|
|
68
|
-
'OBSERVABILITY: Metrics, Logs, Traces (OpenTelemetry).',
|
|
69
|
-
'SECURITY: Least privilege access. Secrets management.',
|
|
70
|
-
],
|
|
71
|
-
},
|
|
72
|
-
qa: {
|
|
73
|
-
title: 'Quality Assurance Standards',
|
|
74
|
-
rules: [
|
|
75
|
-
'PYRAMID: Many unit tests, some integration, few E2E.',
|
|
76
|
-
'COVERAGE: Critical paths must be tested.',
|
|
77
|
-
'REALISM: Test with realistic data and scenarios.',
|
|
78
|
-
'SPEED: Fast feedback loops. Parallel execution.',
|
|
79
|
-
],
|
|
80
|
-
},
|
|
81
|
-
architecture: {
|
|
82
|
-
title: 'System Architecture Standards',
|
|
83
|
-
rules: [
|
|
84
|
-
'MODULARITY: High cohesion, low coupling.',
|
|
85
|
-
'EVOLVABILITY: Easy to change. Hard to break.',
|
|
86
|
-
'SIMPLICITY: Choose boring technology. Innovation tokens are limited.',
|
|
87
|
-
'DOCS: Architecture Decision Records (ADRs).',
|
|
88
|
-
],
|
|
89
|
-
},
|
|
90
|
-
},
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Get standards for a specific domain
|
|
94
|
-
*/
|
|
95
|
-
getStandards(domain?: string): Standards {
|
|
96
|
-
const key = domain?.toLowerCase()
|
|
97
|
-
const specific = (key && this.domains[key]) || { title: 'General Standards', rules: [] }
|
|
98
|
-
|
|
99
|
-
return {
|
|
100
|
-
title: specific.title,
|
|
101
|
-
rules: [...this.general, ...specific.rules],
|
|
102
|
-
}
|
|
103
|
-
},
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
export default ProductStandards
|
|
@@ -1,167 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SmartCache - Intelligent Persistent Cache for Agents
|
|
3
|
-
*
|
|
4
|
-
* Cache with specific keys: {projectId}-{domain}-{techStack}
|
|
5
|
-
* Persists to disk for cross-session caching
|
|
6
|
-
* Invalidates only when stack changes
|
|
7
|
-
*
|
|
8
|
-
* @version 1.0.0
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import fs from 'fs/promises'
|
|
12
|
-
import path from 'path'
|
|
13
|
-
import os from 'os'
|
|
14
|
-
import crypto from 'crypto'
|
|
15
|
-
import log from '../utils/logger'
|
|
16
|
-
|
|
17
|
-
interface CacheStats {
|
|
18
|
-
size: number
|
|
19
|
-
keys: string[]
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
class SmartCache {
|
|
23
|
-
projectId: string | null
|
|
24
|
-
memoryCache: Map<string, unknown>
|
|
25
|
-
cacheDir: string
|
|
26
|
-
cacheFile: string
|
|
27
|
-
|
|
28
|
-
constructor(projectId: string | null = null) {
|
|
29
|
-
this.projectId = projectId
|
|
30
|
-
this.memoryCache = new Map()
|
|
31
|
-
this.cacheDir = path.join(os.homedir(), '.prjct-cli', 'cache')
|
|
32
|
-
this.cacheFile = projectId
|
|
33
|
-
? path.join(this.cacheDir, `agents-${projectId}.json`)
|
|
34
|
-
: path.join(this.cacheDir, 'agents-global.json')
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Initialize cache - load from disk
|
|
39
|
-
*/
|
|
40
|
-
async initialize(): Promise<void> {
|
|
41
|
-
try {
|
|
42
|
-
await fs.mkdir(this.cacheDir, { recursive: true })
|
|
43
|
-
await this.loadFromDisk()
|
|
44
|
-
} catch {
|
|
45
|
-
// Cache file doesn't exist yet - that's ok
|
|
46
|
-
this.memoryCache = new Map()
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Generate cache key
|
|
52
|
-
* Format: {projectId}-{domain}-{techStackHash}
|
|
53
|
-
*/
|
|
54
|
-
generateKey(projectId: string | null, domain: string, techStack: Record<string, unknown> = {}): string {
|
|
55
|
-
const techString = JSON.stringify(techStack)
|
|
56
|
-
const techHash = crypto.createHash('md5').update(techString).digest('hex').substring(0, 8)
|
|
57
|
-
return `${projectId || 'global'}-${domain}-${techHash}`
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Get from cache
|
|
62
|
-
*/
|
|
63
|
-
async get<T = unknown>(key: string): Promise<T | null> {
|
|
64
|
-
// Check memory first
|
|
65
|
-
if (this.memoryCache.has(key)) {
|
|
66
|
-
return this.memoryCache.get(key) as T
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// Load from disk if not in memory
|
|
70
|
-
await this.loadFromDisk()
|
|
71
|
-
return (this.memoryCache.get(key) as T) || null
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Set in cache
|
|
76
|
-
*/
|
|
77
|
-
async set(key: string, value: unknown): Promise<void> {
|
|
78
|
-
// Set in memory
|
|
79
|
-
this.memoryCache.set(key, value)
|
|
80
|
-
|
|
81
|
-
// Persist to disk (async, don't wait)
|
|
82
|
-
this.persistToDisk().catch((err) => {
|
|
83
|
-
log.error('Cache persist error:', err.message)
|
|
84
|
-
})
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Check if key exists
|
|
89
|
-
*/
|
|
90
|
-
async has(key: string): Promise<boolean> {
|
|
91
|
-
if (this.memoryCache.has(key)) {
|
|
92
|
-
return true
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
await this.loadFromDisk()
|
|
96
|
-
return this.memoryCache.has(key)
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Clear cache
|
|
101
|
-
*/
|
|
102
|
-
async clear(): Promise<void> {
|
|
103
|
-
this.memoryCache.clear()
|
|
104
|
-
try {
|
|
105
|
-
await fs.unlink(this.cacheFile)
|
|
106
|
-
} catch {
|
|
107
|
-
// File doesn't exist - that's ok
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Invalidate cache for a project (when stack changes)
|
|
113
|
-
*/
|
|
114
|
-
async invalidateProject(projectId: string): Promise<void> {
|
|
115
|
-
const keysToDelete: string[] = []
|
|
116
|
-
for (const key of this.memoryCache.keys()) {
|
|
117
|
-
if (key.startsWith(`${projectId}-`)) {
|
|
118
|
-
keysToDelete.push(key)
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
keysToDelete.forEach((key) => this.memoryCache.delete(key))
|
|
123
|
-
await this.persistToDisk()
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Load cache from disk
|
|
128
|
-
*/
|
|
129
|
-
async loadFromDisk(): Promise<void> {
|
|
130
|
-
try {
|
|
131
|
-
const content = await fs.readFile(this.cacheFile, 'utf-8')
|
|
132
|
-
const data = JSON.parse(content) as Record<string, unknown>
|
|
133
|
-
|
|
134
|
-
// Restore to memory cache
|
|
135
|
-
for (const [key, value] of Object.entries(data)) {
|
|
136
|
-
this.memoryCache.set(key, value)
|
|
137
|
-
}
|
|
138
|
-
} catch {
|
|
139
|
-
// File doesn't exist or invalid - start fresh
|
|
140
|
-
this.memoryCache = new Map()
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* Persist cache to disk
|
|
146
|
-
*/
|
|
147
|
-
async persistToDisk(): Promise<void> {
|
|
148
|
-
try {
|
|
149
|
-
const data = Object.fromEntries(this.memoryCache)
|
|
150
|
-
await fs.writeFile(this.cacheFile, JSON.stringify(data, null, 2), 'utf-8')
|
|
151
|
-
} catch {
|
|
152
|
-
// Fail silently - cache is best effort
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* Get cache statistics
|
|
158
|
-
*/
|
|
159
|
-
getStats(): CacheStats {
|
|
160
|
-
return {
|
|
161
|
-
size: this.memoryCache.size,
|
|
162
|
-
keys: Array.from(this.memoryCache.keys()),
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
export default SmartCache
|