prjct-cli 0.4.7 โ 0.4.9
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 +44 -0
- package/core/command-installer.js +97 -8
- package/core/commands.js +312 -55
- package/core/editors-config.js +29 -0
- package/package.json +1 -1
- package/scripts/post-install.js +0 -28
package/CHANGELOG.md
CHANGED
|
@@ -15,6 +15,50 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
15
15
|
- Cross-platform file operations
|
|
16
16
|
- Windows Terminal integration
|
|
17
17
|
|
|
18
|
+
## [0.4.9] - 2025-10-02
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
- **Editor Uninstallation** - `prjct start` now allows removing editors
|
|
22
|
+
- Interactive checkbox selection shows currently installed editors
|
|
23
|
+
- Uncheck an editor to remove all prjct commands from it
|
|
24
|
+
- Automatically cleans up `~/.prjct-cli/config/installed-editors.json`
|
|
25
|
+
- User has full control over which editors have prjct commands
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
- **`prjct start` Never Blocks** - Removed "already set up" error
|
|
29
|
+
- Can run `prjct start` anytime to reconfigure editors
|
|
30
|
+
- Shows beautiful ASCII art every time
|
|
31
|
+
- User decides which editors to keep/remove
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
- **Interactive Prompts** - Better UX for editor selection
|
|
35
|
+
- Pre-selects currently installed editors
|
|
36
|
+
- Clear message: "Select AI editors (uncheck to remove)"
|
|
37
|
+
- Allows selecting 0 editors (removes all)
|
|
38
|
+
|
|
39
|
+
## [0.4.8] - 2025-10-02
|
|
40
|
+
|
|
41
|
+
### Added
|
|
42
|
+
- **Intelligent Project Initialization** - `prjct init` now works seamlessly in all scenarios
|
|
43
|
+
- Preserves existing project IDs when re-initializing
|
|
44
|
+
- Automatically merges local data with global structure
|
|
45
|
+
- Smart fusion of markdown files (now.md, next.md, shipped.md, ideas.md)
|
|
46
|
+
- Chronological merging of memory/context.jsonl entries
|
|
47
|
+
- Cleans up local `.prjct/` directory, keeping only `prjct.config.json`
|
|
48
|
+
- Multiple developers can initialize the same project without conflicts
|
|
49
|
+
|
|
50
|
+
### Fixed
|
|
51
|
+
- **Auto-Migration Removed** - Disabled intrusive automatic migration
|
|
52
|
+
- Migration no longer runs automatically during `prjct init` or any command
|
|
53
|
+
- Users can run `prjct migrate-all` manually if needed
|
|
54
|
+
- Removes blocking behavior that prevented normal usage
|
|
55
|
+
- Cleaner, non-intrusive installation and update experience
|
|
56
|
+
|
|
57
|
+
### Changed
|
|
58
|
+
- **`prjct init` Never Blocks** - Removed "already initialized" error
|
|
59
|
+
- Always allows re-initialization with intelligent data fusion
|
|
60
|
+
- Global architecture enables multi-developer workflows
|
|
61
|
+
|
|
18
62
|
## [0.4.7] - 2025-10-02
|
|
19
63
|
|
|
20
64
|
### Fixed
|
|
@@ -480,24 +480,93 @@ For detailed implementation, see prjct-cli documentation.
|
|
|
480
480
|
return await this.installToSelected(detectedEditors, forceUpdate)
|
|
481
481
|
}
|
|
482
482
|
|
|
483
|
+
/**
|
|
484
|
+
* Uninstall commands from a specific editor
|
|
485
|
+
* @param {string} editorKey - Editor key (claude, cursor, windsurf, codex)
|
|
486
|
+
* @returns {Promise<Object>} Uninstall result
|
|
487
|
+
*/
|
|
488
|
+
async uninstallFromEditor(editorKey) {
|
|
489
|
+
const chalk = require('chalk')
|
|
490
|
+
const editorsConfig = require('./editors-config')
|
|
491
|
+
|
|
492
|
+
const editor = this.editors[editorKey]
|
|
493
|
+
if (!editor) {
|
|
494
|
+
return {
|
|
495
|
+
success: false,
|
|
496
|
+
message: `Unknown editor: ${editorKey}`,
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
try {
|
|
501
|
+
// Delete commands from editor
|
|
502
|
+
const commandsPath = editor.path
|
|
503
|
+
const fs = require('fs').promises
|
|
504
|
+
|
|
505
|
+
// Get list of prjct commands
|
|
506
|
+
const commands = ['init', 'now', 'done', 'ship', 'next', 'idea', 'recap', 'progress', 'stuck', 'context',
|
|
507
|
+
'cleanup', 'design', 'task', 'git', 'test', 'roadmap', 'fix', 'analyze']
|
|
508
|
+
|
|
509
|
+
let deletedCount = 0
|
|
510
|
+
|
|
511
|
+
for (const command of commands) {
|
|
512
|
+
const commandFile = path.join(commandsPath, `p:${command}.md`)
|
|
513
|
+
try {
|
|
514
|
+
await fs.unlink(commandFile)
|
|
515
|
+
deletedCount++
|
|
516
|
+
} catch {
|
|
517
|
+
// Command file doesn't exist, skip
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// Remove editor from tracked list
|
|
522
|
+
await editorsConfig.removeTrackedEditor(editorKey)
|
|
523
|
+
|
|
524
|
+
console.log(chalk.yellow(`๐๏ธ Removed ${deletedCount} commands from ${editor.name}`))
|
|
525
|
+
|
|
526
|
+
return {
|
|
527
|
+
success: true,
|
|
528
|
+
message: `Removed commands from ${editor.name}`,
|
|
529
|
+
deleted: deletedCount,
|
|
530
|
+
}
|
|
531
|
+
} catch (error) {
|
|
532
|
+
console.error(chalk.red(`โ Error removing commands from ${editor.name}:`), error.message)
|
|
533
|
+
return {
|
|
534
|
+
success: false,
|
|
535
|
+
message: `Failed to remove commands: ${error.message}`,
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
483
540
|
/**
|
|
484
541
|
* Interactive installation with user selection using prompts
|
|
485
|
-
* @param {boolean}
|
|
542
|
+
* @param {boolean} allowUninstall - Allow uninstalling editors
|
|
486
543
|
* @returns {Promise<Object>} Installation results
|
|
487
544
|
*/
|
|
488
|
-
async interactiveInstall(
|
|
545
|
+
async interactiveInstall(allowUninstall = false) {
|
|
489
546
|
const prompts = require('prompts')
|
|
547
|
+
const editorsConfig = require('./editors-config')
|
|
490
548
|
|
|
491
549
|
// Detect all editors
|
|
492
550
|
const detected = await this.detectEditors(this.projectPath)
|
|
493
551
|
|
|
552
|
+
// Get currently installed editors if allowUninstall
|
|
553
|
+
let installedEditors = []
|
|
554
|
+
if (allowUninstall) {
|
|
555
|
+
try {
|
|
556
|
+
installedEditors = await editorsConfig.getTrackedEditors()
|
|
557
|
+
} catch {
|
|
558
|
+
// No editors installed yet
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
494
562
|
// Create choices for prompts
|
|
495
563
|
const availableEditors = Object.entries(detected)
|
|
496
564
|
.filter(([_, info]) => info.detected)
|
|
497
565
|
.map(([key, info]) => ({
|
|
498
566
|
title: `${this.editors[key].name} (${info.path})`,
|
|
499
567
|
value: key,
|
|
500
|
-
|
|
568
|
+
// Pre-select if allowUninstall and already installed, otherwise select all
|
|
569
|
+
selected: allowUninstall ? installedEditors.includes(key) : true,
|
|
501
570
|
}))
|
|
502
571
|
|
|
503
572
|
if (availableEditors.length === 0) {
|
|
@@ -513,15 +582,15 @@ For detailed implementation, see prjct-cli documentation.
|
|
|
513
582
|
const response = await prompts({
|
|
514
583
|
type: 'multiselect',
|
|
515
584
|
name: 'selectedEditors',
|
|
516
|
-
message: 'Select AI editors to install commands to:',
|
|
585
|
+
message: allowUninstall ? 'Select AI editors (uncheck to remove):' : 'Select AI editors to install commands to:',
|
|
517
586
|
choices: availableEditors,
|
|
518
|
-
min: 1,
|
|
587
|
+
min: allowUninstall ? 0 : 1, // Allow 0 selections if uninstalling
|
|
519
588
|
hint: '- Space to select. Return to submit',
|
|
520
589
|
instructions: false,
|
|
521
590
|
})
|
|
522
591
|
|
|
523
592
|
// Check if user cancelled
|
|
524
|
-
if (
|
|
593
|
+
if (response.selectedEditors === undefined) {
|
|
525
594
|
return {
|
|
526
595
|
success: false,
|
|
527
596
|
message: 'Installation cancelled by user',
|
|
@@ -530,8 +599,28 @@ For detailed implementation, see prjct-cli documentation.
|
|
|
530
599
|
}
|
|
531
600
|
}
|
|
532
601
|
|
|
533
|
-
|
|
534
|
-
|
|
602
|
+
const selectedEditors = response.selectedEditors || []
|
|
603
|
+
|
|
604
|
+
// If allowUninstall, handle editors that were deselected
|
|
605
|
+
if (allowUninstall) {
|
|
606
|
+
const deselectedEditors = installedEditors.filter(e => !selectedEditors.includes(e))
|
|
607
|
+
|
|
608
|
+
for (const editorKey of deselectedEditors) {
|
|
609
|
+
await this.uninstallFromEditor(editorKey)
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// Install to selected editors (or return success if none selected)
|
|
614
|
+
if (selectedEditors.length === 0) {
|
|
615
|
+
return {
|
|
616
|
+
success: true,
|
|
617
|
+
message: 'All editors removed',
|
|
618
|
+
editors: [],
|
|
619
|
+
results: {},
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
return await this.installToSelected(selectedEditors, true) // Force update
|
|
535
624
|
}
|
|
536
625
|
|
|
537
626
|
/**
|
package/core/commands.js
CHANGED
|
@@ -192,8 +192,6 @@ class PrjctCommands {
|
|
|
192
192
|
|
|
193
193
|
this.agent = new Agent()
|
|
194
194
|
|
|
195
|
-
await this.checkAndRunAutoMigration()
|
|
196
|
-
|
|
197
195
|
// Check for updates in background (non-blocking)
|
|
198
196
|
this.checkAndNotifyUpdates().catch(() => {
|
|
199
197
|
// Fail silently - don't interrupt workflow
|
|
@@ -316,41 +314,46 @@ class PrjctCommands {
|
|
|
316
314
|
async init(projectPath = process.cwd()) {
|
|
317
315
|
try {
|
|
318
316
|
await this.initializeAgent()
|
|
319
|
-
|
|
320
|
-
if (await configManager.isConfigured(projectPath)) {
|
|
321
|
-
return {
|
|
322
|
-
success: false,
|
|
323
|
-
message: this.agent.formatResponse('Project already initialized!', 'warning'),
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
|
|
327
317
|
const author = await authorDetector.detect()
|
|
328
318
|
|
|
329
|
-
|
|
330
|
-
|
|
319
|
+
// Check if local config exists
|
|
320
|
+
const isConfigured = await configManager.isConfigured(projectPath)
|
|
321
|
+
let projectId
|
|
322
|
+
let existingLocalData = null
|
|
323
|
+
let fusionPerformed = false
|
|
331
324
|
|
|
332
|
-
if (
|
|
333
|
-
|
|
334
|
-
const
|
|
335
|
-
|
|
325
|
+
if (isConfigured) {
|
|
326
|
+
// Use existing ID from local config
|
|
327
|
+
const existingConfig = await configManager.readConfig(projectPath)
|
|
328
|
+
projectId = existingConfig.projectId
|
|
329
|
+
console.log(`๐ Using existing project ID: ${projectId}`)
|
|
336
330
|
|
|
331
|
+
// Check if there's local data to merge
|
|
332
|
+
const localPrjctPath = path.join(projectPath, '.prjct')
|
|
337
333
|
try {
|
|
338
|
-
const
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
})
|
|
343
|
-
migrationPerformed = migrationResult.success
|
|
334
|
+
const localFiles = await fs.readdir(localPrjctPath)
|
|
335
|
+
if (localFiles.length > 1) { // More than just prjct.config.json
|
|
336
|
+
existingLocalData = await this.collectLocalData(localPrjctPath)
|
|
337
|
+
}
|
|
344
338
|
} catch (error) {
|
|
345
|
-
|
|
339
|
+
// No local data to merge
|
|
346
340
|
}
|
|
341
|
+
} else {
|
|
342
|
+
// Generate new ID based on path hash
|
|
343
|
+
const config = await configManager.createConfig(projectPath, author)
|
|
344
|
+
projectId = config.projectId
|
|
345
|
+
console.log(`โจ Created new project ID: ${projectId}`)
|
|
347
346
|
}
|
|
348
347
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
348
|
+
// Ensure global structure exists
|
|
349
|
+
await pathManager.ensureProjectStructure(projectId)
|
|
350
|
+
const globalPath = pathManager.getGlobalProjectPath(projectId)
|
|
351
|
+
|
|
352
|
+
// Check if global data exists
|
|
353
|
+
const globalExists = await this.checkGlobalDataExists(globalPath)
|
|
353
354
|
|
|
355
|
+
if (!globalExists) {
|
|
356
|
+
// Create fresh global structure
|
|
354
357
|
const files = {
|
|
355
358
|
'core/now.md': '# NOW\n\nNo current task. Use `/p:now` to set focus.\n',
|
|
356
359
|
'core/next.md': '# NEXT\n\n## Priority Queue\n\n',
|
|
@@ -362,22 +365,25 @@ class PrjctCommands {
|
|
|
362
365
|
'memory/context.jsonl': '',
|
|
363
366
|
}
|
|
364
367
|
|
|
365
|
-
const globalPath = pathManager.getGlobalProjectPath(projectId)
|
|
366
368
|
for (const [filePath, content] of Object.entries(files)) {
|
|
367
369
|
await this.agent.writeFile(path.join(globalPath, filePath), content)
|
|
368
370
|
}
|
|
371
|
+
console.log('โ
Created global structure')
|
|
372
|
+
} else {
|
|
373
|
+
console.log('โ
Using existing global data')
|
|
369
374
|
}
|
|
370
375
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
376
|
+
// Merge local data if exists
|
|
377
|
+
if (existingLocalData) {
|
|
378
|
+
console.log('๐ Merging local data with global...')
|
|
379
|
+
await this.mergeProjectData(existingLocalData, globalPath)
|
|
380
|
+
fusionPerformed = true
|
|
381
|
+
}
|
|
374
382
|
|
|
375
|
-
|
|
383
|
+
// Clean up local data (keep only prjct.config.json)
|
|
384
|
+
await this.cleanupLocalData(projectPath)
|
|
376
385
|
|
|
377
|
-
const
|
|
378
|
-
const editorsInstalled = installResult.success
|
|
379
|
-
? `\n๐ค Commands installed to: ${installResult.message.split('Editors: ')[1]?.split('\n')[0] || 'selected editors'}`
|
|
380
|
-
: ''
|
|
386
|
+
const projectInfo = await this.detectProjectType(projectPath)
|
|
381
387
|
|
|
382
388
|
let analysisMessage = ''
|
|
383
389
|
const hasExistingCode = await this.detectExistingCode(projectPath)
|
|
@@ -403,13 +409,14 @@ class PrjctCommands {
|
|
|
403
409
|
}
|
|
404
410
|
|
|
405
411
|
const displayPath = pathManager.getDisplayPath(globalPath)
|
|
412
|
+
const fusionMessage = fusionPerformed ? '\n๐ Merged local data with global' : ''
|
|
406
413
|
const message =
|
|
407
414
|
`Initializing prjct v${VERSION} for ${this.agentInfo.name}...\n` +
|
|
408
|
-
`โ
|
|
409
|
-
|
|
415
|
+
`โ
Project ID: ${projectId}\n` +
|
|
416
|
+
`โ
Global data: ${displayPath}\n` +
|
|
410
417
|
`๐ค Author: ${authorDetector.formatAuthor(author)}\n` +
|
|
411
418
|
`๐ Project: ${projectInfo}` +
|
|
412
|
-
|
|
419
|
+
fusionMessage +
|
|
413
420
|
analysisMessage +
|
|
414
421
|
`\n\nReady! Start with ${this.agentInfo.config.commandPrefix}now "your first task"`
|
|
415
422
|
|
|
@@ -426,6 +433,270 @@ class PrjctCommands {
|
|
|
426
433
|
}
|
|
427
434
|
}
|
|
428
435
|
|
|
436
|
+
/**
|
|
437
|
+
* Check if global data exists for a project
|
|
438
|
+
*
|
|
439
|
+
* @param {string} globalPath - Global project path
|
|
440
|
+
* @returns {Promise<boolean>} True if global data exists
|
|
441
|
+
* @private
|
|
442
|
+
*/
|
|
443
|
+
async checkGlobalDataExists(globalPath) {
|
|
444
|
+
try {
|
|
445
|
+
const nowFile = path.join(globalPath, 'core', 'now.md')
|
|
446
|
+
await fs.access(nowFile)
|
|
447
|
+
return true
|
|
448
|
+
} catch {
|
|
449
|
+
return false
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Collect all data from local .prjct directory
|
|
455
|
+
*
|
|
456
|
+
* @param {string} localPrjctPath - Path to local .prjct directory
|
|
457
|
+
* @returns {Promise<Object>} Object with local data organized by category
|
|
458
|
+
* @private
|
|
459
|
+
*/
|
|
460
|
+
async collectLocalData(localPrjctPath) {
|
|
461
|
+
const data = {
|
|
462
|
+
core: {},
|
|
463
|
+
progress: {},
|
|
464
|
+
planning: {},
|
|
465
|
+
memory: {},
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
try {
|
|
469
|
+
// Collect core files
|
|
470
|
+
const coreFiles = ['now.md', 'next.md', 'context.md']
|
|
471
|
+
for (const file of coreFiles) {
|
|
472
|
+
try {
|
|
473
|
+
const filePath = path.join(localPrjctPath, file)
|
|
474
|
+
data.core[file] = await this.agent.readFile(filePath)
|
|
475
|
+
} catch {
|
|
476
|
+
// File doesn't exist locally
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Collect progress files
|
|
481
|
+
const progressFiles = ['shipped.md', 'metrics.md']
|
|
482
|
+
for (const file of progressFiles) {
|
|
483
|
+
try {
|
|
484
|
+
const filePath = path.join(localPrjctPath, file)
|
|
485
|
+
data.progress[file] = await this.agent.readFile(filePath)
|
|
486
|
+
} catch {
|
|
487
|
+
// File doesn't exist locally
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Collect planning files
|
|
492
|
+
const planningFiles = ['ideas.md', 'roadmap.md']
|
|
493
|
+
for (const file of planningFiles) {
|
|
494
|
+
try {
|
|
495
|
+
const filePath = path.join(localPrjctPath, file)
|
|
496
|
+
data.planning[file] = await this.agent.readFile(filePath)
|
|
497
|
+
} catch {
|
|
498
|
+
// File doesn't exist locally
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// Collect memory
|
|
503
|
+
try {
|
|
504
|
+
const memoryPath = path.join(localPrjctPath, 'memory.jsonl')
|
|
505
|
+
data.memory['context.jsonl'] = await this.agent.readFile(memoryPath)
|
|
506
|
+
} catch {
|
|
507
|
+
// Memory doesn't exist locally
|
|
508
|
+
}
|
|
509
|
+
} catch (error) {
|
|
510
|
+
console.error('[prjct] Error collecting local data:', error.message)
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
return data
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Merge local project data with global data
|
|
518
|
+
*
|
|
519
|
+
* @param {Object} localData - Local data collected from .prjct
|
|
520
|
+
* @param {string} globalPath - Global project path
|
|
521
|
+
* @returns {Promise<void>}
|
|
522
|
+
* @private
|
|
523
|
+
*/
|
|
524
|
+
async mergeProjectData(localData, globalPath) {
|
|
525
|
+
try {
|
|
526
|
+
// Merge core files
|
|
527
|
+
for (const [filename, localContent] of Object.entries(localData.core)) {
|
|
528
|
+
if (!localContent) continue
|
|
529
|
+
|
|
530
|
+
const globalFilePath = path.join(globalPath, 'core', filename)
|
|
531
|
+
let globalContent = ''
|
|
532
|
+
try {
|
|
533
|
+
globalContent = await this.agent.readFile(globalFilePath)
|
|
534
|
+
} catch {
|
|
535
|
+
// Global file doesn't exist yet
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const merged = this.intelligentMerge(localContent, globalContent, filename)
|
|
539
|
+
await this.agent.writeFile(globalFilePath, merged)
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// Merge progress files
|
|
543
|
+
for (const [filename, localContent] of Object.entries(localData.progress)) {
|
|
544
|
+
if (!localContent) continue
|
|
545
|
+
|
|
546
|
+
const globalFilePath = path.join(globalPath, 'progress', filename)
|
|
547
|
+
let globalContent = ''
|
|
548
|
+
try {
|
|
549
|
+
globalContent = await this.agent.readFile(globalFilePath)
|
|
550
|
+
} catch {
|
|
551
|
+
// Global file doesn't exist yet
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
const merged = this.intelligentMerge(localContent, globalContent, filename)
|
|
555
|
+
await this.agent.writeFile(globalFilePath, merged)
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// Merge planning files
|
|
559
|
+
for (const [filename, localContent] of Object.entries(localData.planning)) {
|
|
560
|
+
if (!localContent) continue
|
|
561
|
+
|
|
562
|
+
const globalFilePath = path.join(globalPath, 'planning', filename)
|
|
563
|
+
let globalContent = ''
|
|
564
|
+
try {
|
|
565
|
+
globalContent = await this.agent.readFile(globalFilePath)
|
|
566
|
+
} catch {
|
|
567
|
+
// Global file doesn't exist yet
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
const merged = this.intelligentMerge(localContent, globalContent, filename)
|
|
571
|
+
await this.agent.writeFile(globalFilePath, merged)
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// Merge memory (chronologically)
|
|
575
|
+
if (localData.memory['context.jsonl']) {
|
|
576
|
+
const globalMemoryPath = path.join(globalPath, 'memory', 'context.jsonl')
|
|
577
|
+
let globalMemory = ''
|
|
578
|
+
try {
|
|
579
|
+
globalMemory = await this.agent.readFile(globalMemoryPath)
|
|
580
|
+
} catch {
|
|
581
|
+
// Global memory doesn't exist yet
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
const merged = this.mergeMemory(localData.memory['context.jsonl'], globalMemory)
|
|
585
|
+
await this.agent.writeFile(globalMemoryPath, merged)
|
|
586
|
+
}
|
|
587
|
+
} catch (error) {
|
|
588
|
+
console.error('[prjct] Error merging data:', error.message)
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Intelligent merge of markdown content
|
|
594
|
+
*
|
|
595
|
+
* @param {string} localContent - Content from local file
|
|
596
|
+
* @param {string} globalContent - Content from global file
|
|
597
|
+
* @param {string} filename - Name of the file being merged
|
|
598
|
+
* @returns {string} Merged content
|
|
599
|
+
* @private
|
|
600
|
+
*/
|
|
601
|
+
intelligentMerge(localContent, globalContent, filename) {
|
|
602
|
+
// If global is empty, use local
|
|
603
|
+
if (!globalContent || globalContent.trim() === '' || globalContent.includes('No current task')) {
|
|
604
|
+
return localContent
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// If local is empty, use global
|
|
608
|
+
if (!localContent || localContent.trim() === '') {
|
|
609
|
+
return globalContent
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// For now.md, prefer local (most recent)
|
|
613
|
+
if (filename === 'now.md') {
|
|
614
|
+
return localContent
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// For shipped.md, next.md, ideas.md, roadmap.md - combine unique entries
|
|
618
|
+
const localLines = localContent.split('\n').filter((l) => l.trim())
|
|
619
|
+
const globalLines = globalContent.split('\n').filter((l) => l.trim())
|
|
620
|
+
|
|
621
|
+
// Keep header from global
|
|
622
|
+
const header = globalLines[0] || localLines[0]
|
|
623
|
+
|
|
624
|
+
// Combine unique content (skip headers and empty lines)
|
|
625
|
+
const allLines = [...globalLines.slice(1), ...localLines.slice(1)]
|
|
626
|
+
const uniqueLines = [...new Set(allLines)]
|
|
627
|
+
|
|
628
|
+
return [header, '', ...uniqueLines].join('\n')
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Merge memory.jsonl files chronologically
|
|
633
|
+
*
|
|
634
|
+
* @param {string} localMemory - Local memory content
|
|
635
|
+
* @param {string} globalMemory - Global memory content
|
|
636
|
+
* @returns {string} Merged memory sorted by timestamp
|
|
637
|
+
* @private
|
|
638
|
+
*/
|
|
639
|
+
mergeMemory(localMemory, globalMemory) {
|
|
640
|
+
if (!globalMemory) return localMemory
|
|
641
|
+
if (!localMemory) return globalMemory
|
|
642
|
+
|
|
643
|
+
const localEntries = localMemory.split('\n').filter((l) => l.trim())
|
|
644
|
+
const globalEntries = globalMemory.split('\n').filter((l) => l.trim())
|
|
645
|
+
|
|
646
|
+
const allEntries = [...globalEntries, ...localEntries].map((entry) => {
|
|
647
|
+
try {
|
|
648
|
+
return JSON.parse(entry)
|
|
649
|
+
} catch {
|
|
650
|
+
return null
|
|
651
|
+
}
|
|
652
|
+
}).filter(Boolean)
|
|
653
|
+
|
|
654
|
+
// Sort by timestamp
|
|
655
|
+
allEntries.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp))
|
|
656
|
+
|
|
657
|
+
// Remove duplicates
|
|
658
|
+
const unique = []
|
|
659
|
+
const seen = new Set()
|
|
660
|
+
for (const entry of allEntries) {
|
|
661
|
+
const key = `${entry.timestamp}-${entry.action}`
|
|
662
|
+
if (!seen.has(key)) {
|
|
663
|
+
seen.add(key)
|
|
664
|
+
unique.push(entry)
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
return unique.map((e) => JSON.stringify(e)).join('\n')
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* Clean up local .prjct directory, keeping only prjct.config.json
|
|
673
|
+
*
|
|
674
|
+
* @param {string} projectPath - Project path
|
|
675
|
+
* @returns {Promise<void>}
|
|
676
|
+
* @private
|
|
677
|
+
*/
|
|
678
|
+
async cleanupLocalData(projectPath) {
|
|
679
|
+
try {
|
|
680
|
+
const localPrjctPath = path.join(projectPath, '.prjct')
|
|
681
|
+
const files = await fs.readdir(localPrjctPath, { withFileTypes: true })
|
|
682
|
+
|
|
683
|
+
for (const file of files) {
|
|
684
|
+
if (file.name === 'prjct.config.json') continue
|
|
685
|
+
|
|
686
|
+
const filePath = path.join(localPrjctPath, file.name)
|
|
687
|
+
if (file.isDirectory()) {
|
|
688
|
+
await fs.rm(filePath, { recursive: true, force: true })
|
|
689
|
+
} else {
|
|
690
|
+
await fs.unlink(filePath)
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
console.log('๐งน Cleaned up local data - only prjct.config.json remains')
|
|
695
|
+
} catch (error) {
|
|
696
|
+
console.error('[prjct] Warning: Could not clean up local data:', error.message)
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
429
700
|
/**
|
|
430
701
|
* Set or view current task
|
|
431
702
|
*
|
|
@@ -1949,20 +2220,6 @@ ${diagram}
|
|
|
1949
2220
|
try {
|
|
1950
2221
|
await this.initializeAgent()
|
|
1951
2222
|
|
|
1952
|
-
// Check if already configured
|
|
1953
|
-
const editorsConfig = require('./editors-config')
|
|
1954
|
-
const configExists = await editorsConfig.configExists()
|
|
1955
|
-
|
|
1956
|
-
if (configExists) {
|
|
1957
|
-
return {
|
|
1958
|
-
success: false,
|
|
1959
|
-
message: this.agent.formatResponse(
|
|
1960
|
-
'prjct is already set up!\n\nTo reconfigure editors: prjct setup',
|
|
1961
|
-
'warning'
|
|
1962
|
-
),
|
|
1963
|
-
}
|
|
1964
|
-
}
|
|
1965
|
-
|
|
1966
2223
|
// ASCII Art
|
|
1967
2224
|
const chalk = require('chalk')
|
|
1968
2225
|
console.log('')
|
|
@@ -2006,8 +2263,8 @@ ${diagram}
|
|
|
2006
2263
|
})
|
|
2007
2264
|
console.log('')
|
|
2008
2265
|
|
|
2009
|
-
// Interactive selection
|
|
2010
|
-
const installResult = await commandInstaller.interactiveInstall(
|
|
2266
|
+
// Interactive selection (allow uninstall = true)
|
|
2267
|
+
const installResult = await commandInstaller.interactiveInstall(true)
|
|
2011
2268
|
|
|
2012
2269
|
if (!installResult.success) {
|
|
2013
2270
|
return {
|
package/core/editors-config.js
CHANGED
|
@@ -87,6 +87,35 @@ class EditorsConfig {
|
|
|
87
87
|
return config ? config.editors : []
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Remove an editor from tracked list
|
|
92
|
+
* @param {string} editorKey - Editor key to remove
|
|
93
|
+
* @returns {Promise<boolean>} Success status
|
|
94
|
+
*/
|
|
95
|
+
async removeTrackedEditor(editorKey) {
|
|
96
|
+
try {
|
|
97
|
+
const config = await this.loadConfig()
|
|
98
|
+
if (!config) return false
|
|
99
|
+
|
|
100
|
+
// Remove from editors array
|
|
101
|
+
config.editors = config.editors.filter(e => e !== editorKey)
|
|
102
|
+
|
|
103
|
+
// Remove from paths object
|
|
104
|
+
if (config.paths && config.paths[editorKey]) {
|
|
105
|
+
delete config.paths[editorKey]
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Save updated config
|
|
109
|
+
await fs.mkdir(this.configDir, { recursive: true })
|
|
110
|
+
await fs.writeFile(this.configPath, JSON.stringify(config, null, 2), 'utf-8')
|
|
111
|
+
|
|
112
|
+
return true
|
|
113
|
+
} catch (error) {
|
|
114
|
+
console.error('[editors-config] Error removing tracked editor:', error.message)
|
|
115
|
+
return false
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
90
119
|
/**
|
|
91
120
|
* Get editor paths from configuration
|
|
92
121
|
* @returns {Promise<Object>} Object mapping editor keys to paths
|
package/package.json
CHANGED
package/scripts/post-install.js
CHANGED
|
@@ -81,34 +81,6 @@ async function main() {
|
|
|
81
81
|
// Update version in config
|
|
82
82
|
await editorsConfig.updateVersion(currentVersion)
|
|
83
83
|
|
|
84
|
-
// Auto-migrate legacy projects (v0.1.0 โ v0.4.4)
|
|
85
|
-
try {
|
|
86
|
-
console.log(chalk.cyan('\n๐ Checking for legacy projects to migrate...\n'))
|
|
87
|
-
|
|
88
|
-
const migrator = require('../core/migrator')
|
|
89
|
-
|
|
90
|
-
// Quick scan of common directories (not entire home directory)
|
|
91
|
-
const summary = await migrator.migrateAll({
|
|
92
|
-
deepScan: false, // Only scan common dirs (fast)
|
|
93
|
-
removeLegacy: false, // Keep legacy dirs for safety
|
|
94
|
-
cleanupLegacy: true, // Remove old dirs but keep config
|
|
95
|
-
dryRun: false, // Actually perform migration
|
|
96
|
-
interactive: false // No user prompts during npm install
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
if (summary.successfullyMigrated > 0) {
|
|
100
|
-
console.log(chalk.green(`โ
Migrated ${summary.successfullyMigrated} legacy project(s) to new structure`))
|
|
101
|
-
console.log(chalk.gray(` Data location: ~/.prjct-cli/projects/`))
|
|
102
|
-
} else if (summary.totalFound > 0 && summary.alreadyMigrated === summary.totalFound) {
|
|
103
|
-
console.log(chalk.gray('โน๏ธ All projects already using new structure'))
|
|
104
|
-
}
|
|
105
|
-
} catch (migrationError) {
|
|
106
|
-
// Don't block install if migration fails
|
|
107
|
-
if (process.env.DEBUG) {
|
|
108
|
-
console.error(chalk.yellow('[post-install] Migration warning:'), migrationError.message)
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
84
|
console.log(chalk.cyan(`\nโจ prjct-cli ${currentVersion} is ready!\n`))
|
|
113
85
|
|
|
114
86
|
} catch (error) {
|