prjct-cli 0.4.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 +312 -0
- package/CLAUDE.md +300 -0
- package/LICENSE +21 -0
- package/README.md +424 -0
- package/bin/prjct +214 -0
- package/core/agent-detector.js +249 -0
- package/core/agents/claude-agent.js +250 -0
- package/core/agents/codex-agent.js +256 -0
- package/core/agents/terminal-agent.js +465 -0
- package/core/analyzer.js +596 -0
- package/core/animations-simple.js +240 -0
- package/core/animations.js +277 -0
- package/core/author-detector.js +218 -0
- package/core/capability-installer.js +190 -0
- package/core/command-installer.js +775 -0
- package/core/commands.js +2050 -0
- package/core/config-manager.js +335 -0
- package/core/migrator.js +784 -0
- package/core/path-manager.js +324 -0
- package/core/project-capabilities.js +144 -0
- package/core/session-manager.js +439 -0
- package/core/version.js +107 -0
- package/core/workflow-engine.js +213 -0
- package/core/workflow-prompts.js +192 -0
- package/core/workflow-rules.js +147 -0
- package/package.json +80 -0
- package/scripts/install.sh +433 -0
- package/scripts/verify-installation.sh +158 -0
- package/templates/agents/AGENTS.md +164 -0
- package/templates/commands/analyze.md +125 -0
- package/templates/commands/cleanup.md +102 -0
- package/templates/commands/context.md +105 -0
- package/templates/commands/design.md +113 -0
- package/templates/commands/done.md +44 -0
- package/templates/commands/fix.md +87 -0
- package/templates/commands/git.md +79 -0
- package/templates/commands/help.md +72 -0
- package/templates/commands/idea.md +50 -0
- package/templates/commands/init.md +237 -0
- package/templates/commands/next.md +74 -0
- package/templates/commands/now.md +35 -0
- package/templates/commands/progress.md +92 -0
- package/templates/commands/recap.md +86 -0
- package/templates/commands/roadmap.md +107 -0
- package/templates/commands/ship.md +41 -0
- package/templates/commands/stuck.md +48 -0
- package/templates/commands/task.md +97 -0
- package/templates/commands/test.md +94 -0
- package/templates/commands/workflow.md +224 -0
- package/templates/examples/natural-language-examples.md +320 -0
- package/templates/mcp-config.json +8 -0
- package/templates/workflows/analyze.md +159 -0
- package/templates/workflows/cleanup.md +73 -0
- package/templates/workflows/context.md +72 -0
- package/templates/workflows/design.md +88 -0
- package/templates/workflows/done.md +20 -0
- package/templates/workflows/fix.md +201 -0
- package/templates/workflows/git.md +192 -0
- package/templates/workflows/help.md +13 -0
- package/templates/workflows/idea.md +22 -0
- package/templates/workflows/init.md +80 -0
- package/templates/workflows/natural-language-handler.md +183 -0
- package/templates/workflows/next.md +44 -0
- package/templates/workflows/now.md +19 -0
- package/templates/workflows/progress.md +113 -0
- package/templates/workflows/recap.md +66 -0
- package/templates/workflows/roadmap.md +95 -0
- package/templates/workflows/ship.md +18 -0
- package/templates/workflows/stuck.md +25 -0
- package/templates/workflows/task.md +109 -0
- package/templates/workflows/test.md +243 -0
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
const fs = require('fs').promises
|
|
2
|
+
const path = require('path')
|
|
3
|
+
const pathManager = require('./path-manager')
|
|
4
|
+
const { VERSION } = require('./version')
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* SessionManager - Manages temporal fragmentation of logs and progress data
|
|
8
|
+
*
|
|
9
|
+
* Handles:
|
|
10
|
+
* - Daily session creation and rotation
|
|
11
|
+
* - Writing logs to date-specific directories
|
|
12
|
+
* - Reading historical data across multiple sessions
|
|
13
|
+
* - Session consolidation and queries
|
|
14
|
+
* - Automatic migration from legacy single-file logs
|
|
15
|
+
*
|
|
16
|
+
* @version 0.2.1
|
|
17
|
+
*/
|
|
18
|
+
class SessionManager {
|
|
19
|
+
constructor() {
|
|
20
|
+
this.currentSessionCache = new Map() // Cache current session paths
|
|
21
|
+
this.sessionMetadataCache = new Map() // Cache session metadata
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Get or create current session directory for a project
|
|
26
|
+
*
|
|
27
|
+
* @param {string} projectId - The project identifier
|
|
28
|
+
* @returns {Promise<string>} - Path to today's session directory
|
|
29
|
+
*/
|
|
30
|
+
async getCurrentSession(projectId) {
|
|
31
|
+
const cacheKey = `${projectId}-${this._getTodayKey()}`
|
|
32
|
+
|
|
33
|
+
if (this.currentSessionCache.has(cacheKey)) {
|
|
34
|
+
return this.currentSessionCache.get(cacheKey)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const sessionPath = await pathManager.ensureSessionPath(projectId)
|
|
38
|
+
this.currentSessionCache.set(cacheKey, sessionPath)
|
|
39
|
+
|
|
40
|
+
await this._ensureSessionMetadata(sessionPath)
|
|
41
|
+
|
|
42
|
+
return sessionPath
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Write log entry to current session
|
|
47
|
+
*
|
|
48
|
+
* @param {string} projectId - The project identifier
|
|
49
|
+
* @param {Object} entry - Log entry object
|
|
50
|
+
* @param {string} filename - Target filename (default: context.jsonl)
|
|
51
|
+
* @returns {Promise<void>}
|
|
52
|
+
*/
|
|
53
|
+
async writeToSession(projectId, entry, filename = 'context.jsonl') {
|
|
54
|
+
const sessionPath = await this.getCurrentSession(projectId)
|
|
55
|
+
const filePath = path.join(sessionPath, filename)
|
|
56
|
+
|
|
57
|
+
const logLine = JSON.stringify(entry) + '\n'
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const existing = await fs.readFile(filePath, 'utf-8')
|
|
61
|
+
await fs.writeFile(filePath, existing + logLine, 'utf-8')
|
|
62
|
+
} catch {
|
|
63
|
+
await fs.writeFile(filePath, logLine, 'utf-8')
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
await this._updateSessionMetadata(sessionPath, {
|
|
67
|
+
lastActivity: new Date().toISOString(),
|
|
68
|
+
entryCount: await this._getFileLineCount(filePath),
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Append content to a session file (for markdown files like shipped.md)
|
|
74
|
+
*
|
|
75
|
+
* @param {string} projectId - The project identifier
|
|
76
|
+
* @param {string} content - Content to append
|
|
77
|
+
* @param {string} filename - Target filename
|
|
78
|
+
* @returns {Promise<void>}
|
|
79
|
+
*/
|
|
80
|
+
async appendToSession(projectId, content, filename) {
|
|
81
|
+
const sessionPath = await this.getCurrentSession(projectId)
|
|
82
|
+
const filePath = path.join(sessionPath, filename)
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
const existing = await fs.readFile(filePath, 'utf-8')
|
|
86
|
+
await fs.writeFile(filePath, existing + content, 'utf-8')
|
|
87
|
+
} catch {
|
|
88
|
+
let initialContent = ''
|
|
89
|
+
if (filename === 'shipped.md') {
|
|
90
|
+
initialContent = '# SHIPPED 🚀\n\n'
|
|
91
|
+
}
|
|
92
|
+
await fs.writeFile(filePath, initialContent + content, 'utf-8')
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
await this._updateSessionMetadata(sessionPath, {
|
|
96
|
+
lastActivity: new Date().toISOString(),
|
|
97
|
+
})
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Read logs from current session
|
|
102
|
+
*
|
|
103
|
+
* @param {string} projectId - The project identifier
|
|
104
|
+
* @param {string} filename - Source filename (default: context.jsonl)
|
|
105
|
+
* @returns {Promise<Array<Object>>} - Array of parsed log entries
|
|
106
|
+
*/
|
|
107
|
+
async readCurrentSession(projectId, filename = 'context.jsonl') {
|
|
108
|
+
const sessionPath = await this.getCurrentSession(projectId)
|
|
109
|
+
const filePath = path.join(sessionPath, filename)
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
const content = await fs.readFile(filePath, 'utf-8')
|
|
113
|
+
return this._parseJsonLines(content)
|
|
114
|
+
} catch {
|
|
115
|
+
return []
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Read logs from a specific date range
|
|
121
|
+
*
|
|
122
|
+
* @param {string} projectId - The project identifier
|
|
123
|
+
* @param {Date} fromDate - Start date
|
|
124
|
+
* @param {Date} toDate - End date (defaults to today)
|
|
125
|
+
* @param {string} filename - Source filename (default: context.jsonl)
|
|
126
|
+
* @returns {Promise<Array<Object>>} - Array of parsed log entries from all sessions in range
|
|
127
|
+
*/
|
|
128
|
+
async readSessionRange(projectId, fromDate, toDate = new Date(), filename = 'context.jsonl') {
|
|
129
|
+
const sessions = await pathManager.getSessionsInRange(projectId, fromDate, toDate)
|
|
130
|
+
const allEntries = []
|
|
131
|
+
|
|
132
|
+
for (const session of sessions) {
|
|
133
|
+
const filePath = path.join(session.path, filename)
|
|
134
|
+
try {
|
|
135
|
+
const content = await fs.readFile(filePath, 'utf-8')
|
|
136
|
+
const entries = this._parseJsonLines(content)
|
|
137
|
+
|
|
138
|
+
entries.forEach(entry => {
|
|
139
|
+
entry._sessionDate = session.date
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
allEntries.push(...entries)
|
|
143
|
+
} catch {
|
|
144
|
+
continue
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return allEntries
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Read markdown content from sessions in date range
|
|
153
|
+
*
|
|
154
|
+
* @param {string} projectId - The project identifier
|
|
155
|
+
* @param {Date} fromDate - Start date
|
|
156
|
+
* @param {Date} toDate - End date
|
|
157
|
+
* @param {string} filename - Source filename (e.g., 'shipped.md')
|
|
158
|
+
* @returns {Promise<string>} - Concatenated content from all sessions
|
|
159
|
+
*/
|
|
160
|
+
async readMarkdownRange(projectId, fromDate, toDate, filename) {
|
|
161
|
+
const sessions = await pathManager.getSessionsInRange(projectId, fromDate, toDate)
|
|
162
|
+
const allContent = []
|
|
163
|
+
|
|
164
|
+
for (const session of sessions) {
|
|
165
|
+
const filePath = path.join(session.path, filename)
|
|
166
|
+
try {
|
|
167
|
+
const content = await fs.readFile(filePath, 'utf-8')
|
|
168
|
+
if (content.trim()) {
|
|
169
|
+
allContent.push(`## Session: ${session.year}-${session.month}-${session.day}\n\n${content}`)
|
|
170
|
+
}
|
|
171
|
+
} catch {
|
|
172
|
+
continue
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return allContent.join('\n---\n\n')
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Get recent logs (last N days)
|
|
181
|
+
*
|
|
182
|
+
* @param {string} projectId - The project identifier
|
|
183
|
+
* @param {number} days - Number of days to look back
|
|
184
|
+
* @param {string} filename - Source filename
|
|
185
|
+
* @returns {Promise<Array<Object>>} - Recent log entries
|
|
186
|
+
*/
|
|
187
|
+
async getRecentLogs(projectId, days = 7, filename = 'context.jsonl') {
|
|
188
|
+
const toDate = new Date()
|
|
189
|
+
const fromDate = new Date()
|
|
190
|
+
fromDate.setDate(fromDate.getDate() - days)
|
|
191
|
+
|
|
192
|
+
return await this.readSessionRange(projectId, fromDate, toDate, filename)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Get session statistics
|
|
197
|
+
*
|
|
198
|
+
* @param {string} projectId - The project identifier
|
|
199
|
+
* @param {Date} fromDate - Start date
|
|
200
|
+
* @param {Date} toDate - End date
|
|
201
|
+
* @returns {Promise<Object>} - Statistics object
|
|
202
|
+
*/
|
|
203
|
+
async getSessionStats(projectId, fromDate, toDate) {
|
|
204
|
+
const sessions = await pathManager.getSessionsInRange(projectId, fromDate, toDate)
|
|
205
|
+
|
|
206
|
+
let totalEntries = 0
|
|
207
|
+
let totalShips = 0
|
|
208
|
+
let activeDays = 0
|
|
209
|
+
|
|
210
|
+
for (const session of sessions) {
|
|
211
|
+
const metadata = await this._getSessionMetadata(session.path)
|
|
212
|
+
if (metadata) {
|
|
213
|
+
totalEntries += metadata.entryCount || 0
|
|
214
|
+
totalShips += metadata.shipCount || 0
|
|
215
|
+
if (metadata.entryCount > 0) {
|
|
216
|
+
activeDays++
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
totalSessions: sessions.length,
|
|
223
|
+
activeDays,
|
|
224
|
+
totalEntries,
|
|
225
|
+
totalShips,
|
|
226
|
+
averageEntriesPerDay: activeDays > 0 ? Math.round(totalEntries / activeDays) : 0,
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Migrate legacy single-file logs to session structure
|
|
232
|
+
*
|
|
233
|
+
* @param {string} projectId - The project identifier
|
|
234
|
+
* @param {string} legacyFilePath - Path to legacy log file
|
|
235
|
+
* @param {string} sessionFilename - Target filename in sessions
|
|
236
|
+
* @returns {Promise<Object>} - Migration result
|
|
237
|
+
*/
|
|
238
|
+
async migrateLegacyLogs(projectId, legacyFilePath, sessionFilename) {
|
|
239
|
+
try {
|
|
240
|
+
const content = await fs.readFile(legacyFilePath, 'utf-8')
|
|
241
|
+
|
|
242
|
+
if (sessionFilename.endsWith('.jsonl')) {
|
|
243
|
+
return await this._migrateLegacyJsonl(projectId, content, sessionFilename)
|
|
244
|
+
} else {
|
|
245
|
+
return await this._migrateLegacyMarkdown(projectId, content, sessionFilename)
|
|
246
|
+
}
|
|
247
|
+
} catch (error) {
|
|
248
|
+
return {
|
|
249
|
+
success: false,
|
|
250
|
+
message: `Migration failed: ${error.message}`,
|
|
251
|
+
entriesMigrated: 0,
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Migrate legacy JSONL file
|
|
258
|
+
* @private
|
|
259
|
+
*/
|
|
260
|
+
async _migrateLegacyJsonl(projectId, content, sessionFilename) {
|
|
261
|
+
const entries = this._parseJsonLines(content)
|
|
262
|
+
const sessionGroups = new Map()
|
|
263
|
+
|
|
264
|
+
for (const entry of entries) {
|
|
265
|
+
const date = new Date(entry.timestamp || entry.data?.timestamp || Date.now())
|
|
266
|
+
const dateKey = this._getDateKey(date)
|
|
267
|
+
|
|
268
|
+
if (!sessionGroups.has(dateKey)) {
|
|
269
|
+
sessionGroups.set(dateKey, [])
|
|
270
|
+
}
|
|
271
|
+
sessionGroups.get(dateKey).push(entry)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
let migratedCount = 0
|
|
275
|
+
for (const [dateKey, groupEntries] of sessionGroups) {
|
|
276
|
+
const [year, month, day] = dateKey.split('-')
|
|
277
|
+
const date = new Date(year, month - 1, day)
|
|
278
|
+
const sessionPath = await pathManager.ensureSessionPath(projectId, date)
|
|
279
|
+
const filePath = path.join(sessionPath, sessionFilename)
|
|
280
|
+
|
|
281
|
+
const content = groupEntries.map(e => JSON.stringify(e)).join('\n') + '\n'
|
|
282
|
+
await fs.writeFile(filePath, content, 'utf-8')
|
|
283
|
+
|
|
284
|
+
migratedCount += groupEntries.length
|
|
285
|
+
|
|
286
|
+
await this._ensureSessionMetadata(sessionPath)
|
|
287
|
+
await this._updateSessionMetadata(sessionPath, {
|
|
288
|
+
entryCount: groupEntries.length,
|
|
289
|
+
migrated: true,
|
|
290
|
+
migratedAt: new Date().toISOString(),
|
|
291
|
+
})
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return {
|
|
295
|
+
success: true,
|
|
296
|
+
message: `Migrated ${migratedCount} entries to ${sessionGroups.size} sessions`,
|
|
297
|
+
entriesMigrated: migratedCount,
|
|
298
|
+
sessionsCreated: sessionGroups.size,
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Migrate legacy markdown file
|
|
304
|
+
* @private
|
|
305
|
+
*/
|
|
306
|
+
async _migrateLegacyMarkdown(projectId, content, sessionFilename) {
|
|
307
|
+
const sessionPath = await this.getCurrentSession(projectId)
|
|
308
|
+
const filePath = path.join(sessionPath, sessionFilename)
|
|
309
|
+
|
|
310
|
+
await fs.writeFile(filePath, content, 'utf-8')
|
|
311
|
+
|
|
312
|
+
await this._updateSessionMetadata(sessionPath, {
|
|
313
|
+
migrated: true,
|
|
314
|
+
migratedAt: new Date().toISOString(),
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
return {
|
|
318
|
+
success: true,
|
|
319
|
+
message: 'Migrated markdown content to current session',
|
|
320
|
+
entriesMigrated: 1,
|
|
321
|
+
sessionsCreated: 1,
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Get session metadata
|
|
327
|
+
* @private
|
|
328
|
+
*/
|
|
329
|
+
async _getSessionMetadata(sessionPath) {
|
|
330
|
+
const metadataPath = path.join(sessionPath, 'session-meta.json')
|
|
331
|
+
|
|
332
|
+
if (this.sessionMetadataCache.has(sessionPath)) {
|
|
333
|
+
return this.sessionMetadataCache.get(sessionPath)
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
try {
|
|
337
|
+
const content = await fs.readFile(metadataPath, 'utf-8')
|
|
338
|
+
const metadata = JSON.parse(content)
|
|
339
|
+
this.sessionMetadataCache.set(sessionPath, metadata)
|
|
340
|
+
return metadata
|
|
341
|
+
} catch {
|
|
342
|
+
return null
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Ensure session metadata exists
|
|
348
|
+
* @private
|
|
349
|
+
*/
|
|
350
|
+
async _ensureSessionMetadata(sessionPath) {
|
|
351
|
+
const metadataPath = path.join(sessionPath, 'session-meta.json')
|
|
352
|
+
|
|
353
|
+
try {
|
|
354
|
+
await fs.access(metadataPath)
|
|
355
|
+
} catch {
|
|
356
|
+
// Create initial metadata
|
|
357
|
+
const metadata = {
|
|
358
|
+
created: new Date().toISOString(),
|
|
359
|
+
lastActivity: new Date().toISOString(),
|
|
360
|
+
entryCount: 0,
|
|
361
|
+
shipCount: 0,
|
|
362
|
+
version: VERSION,
|
|
363
|
+
}
|
|
364
|
+
await fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2), 'utf-8')
|
|
365
|
+
this.sessionMetadataCache.set(sessionPath, metadata)
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Update session metadata
|
|
371
|
+
* @private
|
|
372
|
+
*/
|
|
373
|
+
async _updateSessionMetadata(sessionPath, updates) {
|
|
374
|
+
const metadata = await this._getSessionMetadata(sessionPath) || {}
|
|
375
|
+
Object.assign(metadata, updates)
|
|
376
|
+
|
|
377
|
+
const metadataPath = path.join(sessionPath, 'session-meta.json')
|
|
378
|
+
await fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2), 'utf-8')
|
|
379
|
+
|
|
380
|
+
this.sessionMetadataCache.set(sessionPath, metadata)
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Parse JSONL content
|
|
385
|
+
* @private
|
|
386
|
+
*/
|
|
387
|
+
_parseJsonLines(content) {
|
|
388
|
+
const lines = content.split('\n').filter(line => line.trim())
|
|
389
|
+
const entries = []
|
|
390
|
+
|
|
391
|
+
for (const line of lines) {
|
|
392
|
+
try {
|
|
393
|
+
entries.push(JSON.parse(line))
|
|
394
|
+
} catch {
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
return entries
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Get line count from file
|
|
403
|
+
* @private
|
|
404
|
+
*/
|
|
405
|
+
async _getFileLineCount(filePath) {
|
|
406
|
+
try {
|
|
407
|
+
const content = await fs.readFile(filePath, 'utf-8')
|
|
408
|
+
return content.split('\n').filter(line => line.trim()).length
|
|
409
|
+
} catch {
|
|
410
|
+
return 0
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Get today's date key (YYYY-MM-DD)
|
|
416
|
+
* @private
|
|
417
|
+
*/
|
|
418
|
+
_getTodayKey() {
|
|
419
|
+
return this._getDateKey(new Date())
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Get date key for any date (YYYY-MM-DD)
|
|
424
|
+
* @private
|
|
425
|
+
*/
|
|
426
|
+
_getDateKey(date) {
|
|
427
|
+
const year = date.getFullYear()
|
|
428
|
+
const month = (date.getMonth() + 1).toString().padStart(2, '0')
|
|
429
|
+
const day = date.getDate().toString().padStart(2, '0')
|
|
430
|
+
return `${year}-${month}-${day}`
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
clearCache() {
|
|
434
|
+
this.currentSessionCache.clear()
|
|
435
|
+
this.sessionMetadataCache.clear()
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
module.exports = new SessionManager()
|
package/core/version.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
const fs = require('fs')
|
|
2
|
+
const path = require('path')
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Version Manager - Single source of truth for application version
|
|
6
|
+
*
|
|
7
|
+
* Reads version from package.json dynamically to ensure consistency
|
|
8
|
+
* across the entire application.
|
|
9
|
+
*
|
|
10
|
+
* @module version
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
let cachedVersion = null
|
|
14
|
+
let cachedPackageJson = null
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Get the current application version from package.json
|
|
18
|
+
*
|
|
19
|
+
* @returns {string} - Semantic version string (e.g., "0.2.1")
|
|
20
|
+
*/
|
|
21
|
+
function getVersion() {
|
|
22
|
+
if (cachedVersion) {
|
|
23
|
+
return cachedVersion
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const packageJsonPath = path.join(__dirname, '..', 'package.json')
|
|
28
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'))
|
|
29
|
+
cachedVersion = packageJson.version
|
|
30
|
+
cachedPackageJson = packageJson
|
|
31
|
+
return cachedVersion
|
|
32
|
+
} catch (error) {
|
|
33
|
+
console.error('Failed to read version from package.json:', error.message)
|
|
34
|
+
return '0.0.0'
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Get the full package.json object
|
|
40
|
+
*
|
|
41
|
+
* @returns {Object} - Package.json contents
|
|
42
|
+
*/
|
|
43
|
+
function getPackageInfo() {
|
|
44
|
+
if (!cachedPackageJson) {
|
|
45
|
+
getVersion()
|
|
46
|
+
}
|
|
47
|
+
return cachedPackageJson
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Compare two semantic version strings
|
|
52
|
+
*
|
|
53
|
+
* @param {string} v1 - First version (e.g., "0.2.1")
|
|
54
|
+
* @param {string} v2 - Second version (e.g., "0.2.0")
|
|
55
|
+
* @returns {number} - Returns -1 if v1 < v2, 0 if equal, 1 if v1 > v2
|
|
56
|
+
*/
|
|
57
|
+
function compareVersions(v1, v2) {
|
|
58
|
+
const parts1 = v1.split('.').map(Number)
|
|
59
|
+
const parts2 = v2.split('.').map(Number)
|
|
60
|
+
|
|
61
|
+
for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
|
|
62
|
+
const num1 = parts1[i] || 0
|
|
63
|
+
const num2 = parts2[i] || 0
|
|
64
|
+
|
|
65
|
+
if (num1 > num2) return 1
|
|
66
|
+
if (num1 < num2) return -1
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return 0
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Check if a config version is compatible with current version
|
|
74
|
+
*
|
|
75
|
+
* @param {string} configVersion - Version from config file
|
|
76
|
+
* @returns {boolean} - True if compatible
|
|
77
|
+
*/
|
|
78
|
+
function isCompatible(configVersion) {
|
|
79
|
+
const current = getVersion()
|
|
80
|
+
const [currentMajor, currentMinor] = current.split('.').map(Number)
|
|
81
|
+
const [configMajor, configMinor] = configVersion.split('.').map(Number)
|
|
82
|
+
|
|
83
|
+
return currentMajor === configMajor && currentMinor === configMinor
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Check if migration is needed based on version comparison
|
|
88
|
+
*
|
|
89
|
+
* @param {string} fromVersion - Current config version
|
|
90
|
+
* @param {string} toVersion - Target version (defaults to current)
|
|
91
|
+
* @returns {boolean} - True if migration needed
|
|
92
|
+
*/
|
|
93
|
+
function needsMigration(fromVersion, toVersion = null) {
|
|
94
|
+
const target = toVersion || getVersion()
|
|
95
|
+
return compareVersions(fromVersion, target) < 0
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const VERSION = getVersion()
|
|
99
|
+
|
|
100
|
+
module.exports = {
|
|
101
|
+
VERSION,
|
|
102
|
+
getVersion,
|
|
103
|
+
getPackageInfo,
|
|
104
|
+
compareVersions,
|
|
105
|
+
isCompatible,
|
|
106
|
+
needsMigration,
|
|
107
|
+
}
|