prjct-cli 0.4.9 → 0.5.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.
Files changed (49) hide show
  1. package/CHANGELOG.md +316 -0
  2. package/CLAUDE.md +109 -3
  3. package/README.md +228 -93
  4. package/core/agent-detector.js +55 -122
  5. package/core/agent-generator.js +516 -0
  6. package/core/command-installer.js +104 -890
  7. package/core/commands.js +3 -18
  8. package/core/editors-config.js +9 -57
  9. package/core/git-integration.js +401 -0
  10. package/package.json +10 -7
  11. package/scripts/install.sh +0 -1
  12. package/templates/agents/be.template.md +42 -0
  13. package/templates/agents/data.template.md +41 -0
  14. package/templates/agents/devops.template.md +41 -0
  15. package/templates/agents/fe.template.md +42 -0
  16. package/templates/agents/mobile.template.md +41 -0
  17. package/templates/agents/pm.template.md +84 -0
  18. package/templates/agents/qa.template.md +54 -0
  19. package/templates/agents/scribe.template.md +95 -0
  20. package/templates/agents/security.template.md +41 -0
  21. package/templates/agents/ux.template.md +49 -0
  22. package/templates/commands/analyze.md +137 -3
  23. package/templates/commands/done.md +154 -5
  24. package/templates/commands/init.md +61 -3
  25. package/templates/commands/ship.md +146 -6
  26. package/templates/commands/sync.md +220 -0
  27. package/templates/examples/natural-language-examples.md +234 -22
  28. package/core/agents/codex-agent.js +0 -256
  29. package/core/agents/terminal-agent.js +0 -465
  30. package/templates/workflows/analyze.md +0 -159
  31. package/templates/workflows/cleanup.md +0 -73
  32. package/templates/workflows/context.md +0 -72
  33. package/templates/workflows/design.md +0 -88
  34. package/templates/workflows/done.md +0 -20
  35. package/templates/workflows/fix.md +0 -201
  36. package/templates/workflows/git.md +0 -192
  37. package/templates/workflows/help.md +0 -13
  38. package/templates/workflows/idea.md +0 -22
  39. package/templates/workflows/init.md +0 -80
  40. package/templates/workflows/natural-language-handler.md +0 -183
  41. package/templates/workflows/next.md +0 -44
  42. package/templates/workflows/now.md +0 -19
  43. package/templates/workflows/progress.md +0 -113
  44. package/templates/workflows/recap.md +0 -66
  45. package/templates/workflows/roadmap.md +0 -95
  46. package/templates/workflows/ship.md +0 -18
  47. package/templates/workflows/stuck.md +0 -25
  48. package/templates/workflows/task.md +0 -109
  49. package/templates/workflows/test.md +0 -243
@@ -3,95 +3,33 @@ const path = require('path')
3
3
  const os = require('os')
4
4
 
5
5
  /**
6
- * CommandInstaller - Installs prjct commands to AI editors
6
+ * CommandInstaller - Installs prjct commands to Claude (Code + Desktop)
7
7
  *
8
- * Handles installation and synchronization of /p:* commands across
9
- * multiple AI editor environments (Claude Code, Cursor, Codeium, etc.)
8
+ * 100% Claude-focused architecture
9
+ * Handles installation and synchronization of /p:* commands
10
+ * to Claude's native slash command system
10
11
  *
11
- * @version 0.2.1
12
+ * @version 0.5.0
12
13
  */
13
14
  class CommandInstaller {
14
15
  constructor() {
15
16
  this.homeDir = os.homedir()
16
- this.projectPath = process.cwd()
17
-
18
- this.editors = {
19
- claude: {
20
- name: 'Claude Code',
21
- commandsPath: path.join(this.homeDir, '.claude', 'commands', 'p'),
22
- configPath: path.join(this.homeDir, '.claude'),
23
- format: 'slash-commands', // *.md with frontmatter
24
- detected: false,
25
- },
26
- cursor: {
27
- name: 'Cursor AI',
28
- commandsPath: path.join(this.homeDir, '.cursor', 'commands', 'p'),
29
- configPath: path.join(this.homeDir, '.cursor'),
30
- format: 'slash-commands', // *.md with frontmatter
31
- detected: false,
32
- },
33
- codex: {
34
- name: 'OpenAI Codex',
35
- commandsPath: path.join(this.homeDir, '.codex', 'instructions.md'),
36
- configPath: path.join(this.homeDir, '.codex'),
37
- format: 'agents-md', // Single instructions.md file
38
- detected: false,
39
- projectBased: false,
40
- },
41
- windsurf: {
42
- name: 'Windsurf/Codeium',
43
- commandsPath: path.join(this.homeDir, '.windsurf', 'workflows'),
44
- configPath: path.join(this.homeDir, '.windsurf'),
45
- format: 'workflows', // *.md workflows
46
- detected: false,
47
- projectBased: false,
48
- },
49
- }
50
-
17
+ this.claudeCommandsPath = path.join(this.homeDir, '.claude', 'commands', 'p')
18
+ this.claudeConfigPath = path.join(this.homeDir, '.claude')
51
19
  this.templatesDir = path.join(__dirname, '..', 'templates', 'commands')
52
- this.agentsTemplateDir = path.join(__dirname, '..', 'templates', 'agents')
53
- this.workflowsTemplateDir = path.join(__dirname, '..', 'templates', 'workflows')
54
- }
55
-
56
- /**
57
- * Set project path for project-based editors
58
- * @param {string} projectPath - Path to the project
59
- */
60
- setProjectPath(projectPath) {
61
- this.projectPath = projectPath
62
- // codex and windsurf use global paths defined in constructor
63
20
  }
64
21
 
65
22
  /**
66
- * Detect which AI editors are installed
67
- * @param {string} projectPath - Optional project path for project-based editors
68
- * @returns {Promise<Object>} Object with editor detection results
23
+ * Detect if Claude is installed
24
+ * @returns {Promise<boolean>} True if Claude directory exists
69
25
  */
70
- async detectEditors(projectPath = null) {
71
- if (projectPath) {
72
- this.setProjectPath(projectPath)
73
- }
74
-
75
- const results = {}
76
-
77
- for (const [key, editor] of Object.entries(this.editors)) {
78
- try {
79
- await fs.access(editor.configPath)
80
- editor.detected = true
81
-
82
- let commandPath = editor.commandsPath
83
- if (!commandPath && editor.projectBased) {
84
- commandPath = path.join(this.projectPath, 'AGENTS.md')
85
- }
86
-
87
- results[key] = { detected: true, path: commandPath, format: editor.format }
88
- } catch {
89
- editor.detected = false
90
- results[key] = { detected: false, path: null, format: editor.format }
91
- }
26
+ async detectClaude() {
27
+ try {
28
+ await fs.access(this.claudeConfigPath)
29
+ return true
30
+ } catch {
31
+ return false
92
32
  }
93
-
94
- return results
95
33
  }
96
34
 
97
35
  /**
@@ -103,6 +41,7 @@ class CommandInstaller {
103
41
  const files = await fs.readdir(this.templatesDir)
104
42
  return files.filter(f => f.endsWith('.md'))
105
43
  } catch (error) {
44
+ // Fallback to core commands if template directory not accessible
106
45
  return [
107
46
  'init.md',
108
47
  'now.md',
@@ -115,898 +54,173 @@ class CommandInstaller {
115
54
  'stuck.md',
116
55
  'context.md',
117
56
  'analyze.md',
57
+ 'sync.md',
118
58
  'roadmap.md',
119
59
  'task.md',
120
60
  'git.md',
121
61
  'fix.md',
122
62
  'test.md',
63
+ 'cleanup.md',
64
+ 'design.md',
123
65
  ]
124
66
  }
125
67
  }
126
68
 
127
69
  /**
128
- * Generate AGENTS.md content for Codex
129
- * @returns {Promise<string>} AGENTS.md content
130
- */
131
- async generateAgentsMd() {
132
- const templatePath = path.join(this.agentsTemplateDir, 'AGENTS.md')
133
-
134
- try {
135
- return await fs.readFile(templatePath, 'utf-8')
136
- } catch {
137
- const existingPath = path.join(this.projectPath, 'AGENTS.md')
138
- try {
139
- return await fs.readFile(existingPath, 'utf-8')
140
- } catch {
141
- return `# AGENTS.md - OpenAI Codex Configuration
142
-
143
- This file provides guidance to OpenAI Codex when working with this repository.
144
-
145
- ## prjct Commands
146
-
147
- The project uses prjct-cli for project management. All commands access global data in \`~/.prjct-cli/projects/{id}/\`.
148
-
149
- ### /p:init
150
- Initialize prjct in current project. Creates global structure and local config.
151
-
152
- ### /p:now [task]
153
- Set or show current task.
154
- - Read: Show current task from global storage
155
- - Write: Update task in \`~/.prjct-cli/projects/{id}/core/now.md\`
156
-
157
- ### /p:done
158
- Complete current task and clear focus.
159
-
160
- ### /p:ship <feature>
161
- Ship and celebrate a completed feature.
162
-
163
- ### /p:next
164
- Show priority queue of upcoming tasks.
165
-
166
- ### /p:idea <text>
167
- Capture an idea quickly to the backlog.
168
-
169
- ### /p:recap
170
- Show project overview with progress metrics.
171
-
172
- See complete command documentation in the prjct-cli repository.
173
- `
174
- }
175
- }
176
- }
177
-
178
- /**
179
- * Generate Windsurf workflow content
180
- * @param {string} commandName - Command name (e.g., 'now', 'done')
181
- * @returns {Promise<string>} Workflow content
182
- */
183
- async generateWorkflow(commandName) {
184
- const templatePath = path.join(this.workflowsTemplateDir, `${commandName}.md`)
185
-
186
- try {
187
- return await fs.readFile(templatePath, 'utf-8')
188
- } catch {
189
- const invocableName = `p:${commandName}`
190
- return `---
191
- title: prjct ${commandName}
192
- invocable_name: ${invocableName}
193
- description: Execute prjct ${commandName} command
194
- ---
195
-
196
- # Steps
197
-
198
- 1. Read project config from \`.prjct/prjct.config.json\`
199
- 2. Get project ID from config
200
- 3. Execute ${commandName} operation on global data in \`~/.prjct-cli/projects/{id}/\`
201
- 4. Update relevant files in appropriate layers (core, progress, planning, memory)
202
- 5. Log action to memory with timestamp
203
- 6. Display confirmation with next suggested actions
204
-
205
- For detailed implementation, see prjct-cli documentation.
206
- `
207
- }
208
- }
209
-
210
- /**
211
- * Install commands to a specific editor
212
- * @param {string} editorKey - Editor identifier (claude, cursor, codex, windsurf)
213
- * @param {boolean} forceUpdate - Force update existing commands
214
- * @returns {Promise<Object>} Installation result
70
+ * Install commands to Claude
71
+ * @returns {Promise<Object>} Installation results
215
72
  */
216
- async installToEditor(editorKey, forceUpdate = false) {
217
- const editor = this.editors[editorKey]
218
-
219
- if (!editor) {
220
- return { success: false, message: `Unknown editor: ${editorKey}` }
221
- }
222
-
223
- if (!editor.detected) {
224
- return { success: false, message: `${editor.name} not detected` }
225
- }
73
+ async installCommands() {
74
+ const claudeDetected = await this.detectClaude()
226
75
 
227
- try {
228
- switch (editor.format) {
229
- case 'slash-commands':
230
- return await this.installSlashCommands(editorKey, forceUpdate)
231
- case 'agents-md':
232
- return await this.installAgentsMd(editorKey, forceUpdate)
233
- case 'workflows':
234
- return await this.installWorkflows(editorKey, forceUpdate)
235
- default:
236
- return {
237
- success: false,
238
- message: `Unknown format: ${editor.format}`,
239
- }
240
- }
241
- } catch (error) {
76
+ if (!claudeDetected) {
242
77
  return {
243
78
  success: false,
244
- message: `Installation failed for ${editor.name}: ${error.message}`,
79
+ error: 'Claude not detected. Please install Claude Code or Claude Desktop first.',
245
80
  }
246
81
  }
247
- }
248
-
249
- /**
250
- * Install slash commands format (Claude, Cursor)
251
- */
252
- async installSlashCommands(editorKey, forceUpdate) {
253
- const editor = this.editors[editorKey]
254
-
255
- await fs.mkdir(editor.commandsPath, { recursive: true })
256
82
 
257
- const commandFiles = await this.getCommandFiles()
258
- const installed = []
259
- const skipped = []
260
- const updated = []
261
-
262
- for (const filename of commandFiles) {
263
- const targetPath = path.join(editor.commandsPath, filename)
264
- const templatePath = path.join(this.templatesDir, filename)
83
+ try {
84
+ // Ensure commands directory exists
85
+ await fs.mkdir(this.claudeCommandsPath, { recursive: true })
265
86
 
266
- const exists = await this.fileExists(targetPath)
87
+ const commandFiles = await this.getCommandFiles()
88
+ const installed = []
89
+ const errors = []
267
90
 
268
- if (exists && !forceUpdate) {
269
- skipped.push(filename)
270
- continue
271
- }
272
-
273
- let content
274
- try {
275
- content = await fs.readFile(templatePath, 'utf-8')
276
- } catch {
277
- const claudePath = path.join(this.editors.claude.commandsPath, filename)
91
+ for (const file of commandFiles) {
278
92
  try {
279
- content = await fs.readFile(claudePath, 'utf-8')
280
- content = this.updateCommandForGlobalArchitecture(content)
281
- } catch {
282
- skipped.push(filename)
283
- continue
284
- }
285
- }
286
-
287
- await fs.writeFile(targetPath, content, 'utf-8')
93
+ const sourcePath = path.join(this.templatesDir, file)
94
+ const destPath = path.join(this.claudeCommandsPath, file)
288
95
 
289
- if (exists) {
290
- updated.push(filename)
291
- } else {
292
- installed.push(filename)
293
- }
294
- }
295
-
296
- return {
297
- success: true,
298
- editor: editor.name,
299
- format: 'slash-commands',
300
- installed: installed.length,
301
- updated: updated.length,
302
- skipped: skipped.length,
303
- details: { installed, updated, skipped },
304
- }
305
- }
306
-
307
- /**
308
- * Install AGENTS.md format (Codex)
309
- */
310
- async installAgentsMd(editorKey, forceUpdate) {
311
- const editor = this.editors[editorKey]
312
- const targetPath = editor.commandsPath
313
-
314
- const exists = await this.fileExists(targetPath)
315
-
316
- if (exists && !forceUpdate) {
317
- return {
318
- success: true,
319
- editor: editor.name,
320
- format: 'agents-md',
321
- installed: 0,
322
- updated: 0,
323
- skipped: 1,
324
- details: { installed: [], updated: [], skipped: ['AGENTS.md'] },
325
- }
326
- }
327
-
328
- const content = await this.generateAgentsMd()
96
+ const content = await fs.readFile(sourcePath, 'utf-8')
97
+ await fs.writeFile(destPath, content, 'utf-8')
329
98
 
330
- await fs.writeFile(targetPath, content, 'utf-8')
331
-
332
- return {
333
- success: true,
334
- editor: editor.name,
335
- format: 'agents-md',
336
- installed: exists ? 0 : 1,
337
- updated: exists ? 1 : 0,
338
- skipped: 0,
339
- details: {
340
- installed: exists ? [] : ['AGENTS.md'],
341
- updated: exists ? ['AGENTS.md'] : [],
342
- skipped: [],
343
- },
344
- }
345
- }
346
-
347
- /**
348
- * Install workflows format (Windsurf)
349
- */
350
- async installWorkflows(editorKey, forceUpdate) {
351
- const editor = this.editors[editorKey]
352
-
353
- await fs.mkdir(editor.commandsPath, { recursive: true })
354
-
355
- const commandFiles = await this.getCommandFiles()
356
- const commandNames = commandFiles.map(f => f.replace('.md', ''))
357
-
358
- const installed = []
359
- const skipped = []
360
- const updated = []
361
-
362
- for (const commandName of commandNames) {
363
- const filename = `p_${commandName}.md` // e.g., p_now.md
364
- const targetPath = path.join(editor.commandsPath, filename)
365
-
366
- const exists = await this.fileExists(targetPath)
367
-
368
- if (exists && !forceUpdate) {
369
- skipped.push(filename)
370
- continue
371
- }
372
-
373
- const content = await this.generateWorkflow(commandName)
374
-
375
- await fs.writeFile(targetPath, content, 'utf-8')
376
-
377
- if (exists) {
378
- updated.push(filename)
379
- } else {
380
- installed.push(filename)
381
- }
382
- }
383
-
384
- return {
385
- success: true,
386
- editor: editor.name,
387
- format: 'workflows',
388
- installed: installed.length,
389
- updated: updated.length,
390
- skipped: skipped.length,
391
- details: { installed, updated, skipped },
392
- }
393
- }
394
-
395
- /**
396
- * Install commands to selected editors
397
- * @param {string[]} selectedEditors - Array of editor keys to install to
398
- * @param {boolean} forceUpdate - Force update existing commands
399
- * @returns {Promise<Object>} Installation results for selected editors
400
- */
401
- async installToSelected(selectedEditors, forceUpdate = false) {
402
- await this.detectEditors(this.projectPath)
403
-
404
- const results = {}
405
- const installedTo = []
406
- const successfulEditors = []
407
-
408
- for (const editorKey of selectedEditors) {
409
- const editor = this.editors[editorKey]
410
-
411
- if (!editor) {
412
- results[editorKey] = {
413
- success: false,
414
- message: `Unknown editor: ${editorKey}`,
415
- }
416
- continue
417
- }
418
-
419
- if (!editor.detected) {
420
- results[editorKey] = {
421
- success: false,
422
- message: `${editor.name} not detected on this system`,
99
+ installed.push(file.replace('.md', ''))
100
+ } catch (error) {
101
+ errors.push({ file, error: error.message })
423
102
  }
424
- continue
425
103
  }
426
104
 
427
- results[editorKey] = await this.installToEditor(editorKey, forceUpdate)
428
- if (results[editorKey].success) {
429
- installedTo.push(editor.name)
430
- successfulEditors.push(editorKey)
431
- }
432
- }
433
-
434
- if (installedTo.length === 0) {
435
105
  return {
436
- success: false,
437
- message: 'No editors were successfully installed to',
438
- results,
106
+ success: true,
107
+ installed,
108
+ errors,
109
+ path: this.claudeCommandsPath,
439
110
  }
440
- }
441
-
442
- const totalInstalled = Object.values(results)
443
- .reduce((sum, r) => sum + (r.installed || 0), 0)
444
- const totalUpdated = Object.values(results)
445
- .reduce((sum, r) => sum + (r.updated || 0), 0)
446
-
447
- // Always save editor selections to config for tracking updates
448
- if (successfulEditors.length > 0) {
449
- await this.saveEditorConfig(successfulEditors)
450
- }
451
-
452
- return {
453
- success: true,
454
- editors: installedTo,
455
- totalInstalled,
456
- totalUpdated,
457
- results,
458
- }
459
- }
460
-
461
- /**
462
- * Install commands to all detected editors
463
- * @param {boolean} forceUpdate - Force update existing commands
464
- * @returns {Promise<Object>} Installation results for all editors
465
- */
466
- async installToAll(forceUpdate = false) {
467
- const detection = await this.detectEditors(this.projectPath)
468
- const detectedEditors = Object.entries(detection)
469
- .filter(([_, info]) => info.detected)
470
- .map(([key, _]) => key)
471
-
472
- if (detectedEditors.length === 0) {
111
+ } catch (error) {
473
112
  return {
474
113
  success: false,
475
- message: 'No AI editors detected',
476
- results: {},
114
+ error: error.message,
477
115
  }
478
116
  }
479
-
480
- return await this.installToSelected(detectedEditors, forceUpdate)
481
117
  }
482
118
 
483
119
  /**
484
- * Uninstall commands from a specific editor
485
- * @param {string} editorKey - Editor key (claude, cursor, windsurf, codex)
486
- * @returns {Promise<Object>} Uninstall result
120
+ * Uninstall commands from Claude
121
+ * @returns {Promise<Object>} Uninstallation results
487
122
  */
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
-
123
+ async uninstallCommands() {
500
124
  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']
125
+ const commandFiles = await this.getCommandFiles()
126
+ const uninstalled = []
127
+ const errors = []
508
128
 
509
- let deletedCount = 0
510
-
511
- for (const command of commands) {
512
- const commandFile = path.join(commandsPath, `p:${command}.md`)
129
+ for (const file of commandFiles) {
513
130
  try {
514
- await fs.unlink(commandFile)
515
- deletedCount++
516
- } catch {
517
- // Command file doesn't exist, skip
131
+ const filePath = path.join(this.claudeCommandsPath, file)
132
+ await fs.unlink(filePath)
133
+ uninstalled.push(file.replace('.md', ''))
134
+ } catch (error) {
135
+ if (error.code !== 'ENOENT') {
136
+ errors.push({ file, error: error.message })
137
+ }
518
138
  }
519
139
  }
520
140
 
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
-
540
- /**
541
- * Interactive installation with user selection using prompts
542
- * @param {boolean} allowUninstall - Allow uninstalling editors
543
- * @returns {Promise<Object>} Installation results
544
- */
545
- async interactiveInstall(allowUninstall = false) {
546
- const prompts = require('prompts')
547
- const editorsConfig = require('./editors-config')
548
-
549
- // Detect all editors
550
- const detected = await this.detectEditors(this.projectPath)
551
-
552
- // Get currently installed editors if allowUninstall
553
- let installedEditors = []
554
- if (allowUninstall) {
141
+ // Try to remove the /p directory if empty
555
142
  try {
556
- installedEditors = await editorsConfig.getTrackedEditors()
143
+ await fs.rmdir(this.claudeCommandsPath)
557
144
  } catch {
558
- // No editors installed yet
145
+ // Directory not empty or doesn't exist - that's fine
559
146
  }
560
- }
561
-
562
- // Create choices for prompts
563
- const availableEditors = Object.entries(detected)
564
- .filter(([_, info]) => info.detected)
565
- .map(([key, info]) => ({
566
- title: `${this.editors[key].name} (${info.path})`,
567
- value: key,
568
- // Pre-select if allowUninstall and already installed, otherwise select all
569
- selected: allowUninstall ? installedEditors.includes(key) : true,
570
- }))
571
147
 
572
- if (availableEditors.length === 0) {
573
148
  return {
574
- success: false,
575
- message: 'No AI editors detected on this system.\n\nSupported editors:\n • Claude Code (~/.claude)\n • Cursor AI (~/.cursor)\n • Windsurf/Codeium (~/.windsurf)\n • OpenAI Codex (~/.codex)',
576
- editors: [],
577
- results: {},
149
+ success: true,
150
+ uninstalled,
151
+ errors,
578
152
  }
579
- }
580
-
581
- // Show interactive selection prompt
582
- const response = await prompts({
583
- type: 'multiselect',
584
- name: 'selectedEditors',
585
- message: allowUninstall ? 'Select AI editors (uncheck to remove):' : 'Select AI editors to install commands to:',
586
- choices: availableEditors,
587
- min: allowUninstall ? 0 : 1, // Allow 0 selections if uninstalling
588
- hint: '- Space to select. Return to submit',
589
- instructions: false,
590
- })
591
-
592
- // Check if user cancelled
593
- if (response.selectedEditors === undefined) {
153
+ } catch (error) {
594
154
  return {
595
155
  success: false,
596
- message: 'Installation cancelled by user',
597
- editors: [],
598
- results: {},
599
- }
600
- }
601
-
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)
156
+ error: error.message,
610
157
  }
611
158
  }
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
624
159
  }
625
160
 
626
161
  /**
627
- * Update command content to use global architecture
628
- * @param {string} content - Original command content
629
- * @returns {string} Updated content
162
+ * Check if commands are already installed
163
+ * @returns {Promise<Object>} Installation status
630
164
  */
631
- updateCommandForGlobalArchitecture(content) {
632
- let updated = content.replace(
633
- /\.prjct\//g,
634
- '~/.prjct-cli/projects/{id}/',
635
- )
636
-
637
- if (!content.includes('Global Architecture')) {
638
- const frontmatter = content.match(/^---[\s\S]*?---/m)
639
- if (frontmatter) {
640
- const note = `
165
+ async checkInstallation() {
166
+ const claudeDetected = await this.detectClaude()
641
167
 
642
- ## Global Architecture
643
- This command uses the global prjct architecture:
644
- - Data stored in: \`~/.prjct-cli/projects/{id}/\`
645
- - Config stored in: \`{project}/.prjct/prjct.config.json\`
646
- - Commands synchronized across all editors
647
-
648
- `
649
- updated = content.replace(frontmatter[0], frontmatter[0] + note)
168
+ if (!claudeDetected) {
169
+ return {
170
+ installed: false,
171
+ claudeDetected: false,
650
172
  }
651
173
  }
652
174
 
653
- return updated
654
- }
655
-
656
- /**
657
- * Check if a file exists
658
- * @param {string} filePath - Path to check
659
- * @returns {Promise<boolean>} True if file exists
660
- */
661
- async fileExists(filePath) {
662
175
  try {
663
- await fs.access(filePath)
664
- return true
665
- } catch {
666
- return false
667
- }
668
- }
669
-
670
- /**
671
- * Create command templates directory and copy existing commands
672
- * @returns {Promise<Object>} Template creation result
673
- */
674
- async createTemplates() {
675
- try {
676
- await fs.mkdir(this.templatesDir, { recursive: true })
677
-
678
- const claudeCommandsPath = this.editors.claude.commandsPath
679
- const hasClaudeCommands = await this.fileExists(claudeCommandsPath)
680
-
681
- if (!hasClaudeCommands) {
682
- return {
683
- success: false,
684
- message: 'No source commands found. Claude Code commands directory not detected.',
685
- }
686
- }
687
-
688
- const files = await fs.readdir(claudeCommandsPath)
689
- const mdFiles = files.filter(f => f.endsWith('.md'))
690
-
691
- let copied = 0
692
- for (const filename of mdFiles) {
693
- const sourcePath = path.join(claudeCommandsPath, filename)
694
- const targetPath = path.join(this.templatesDir, filename)
695
-
696
- const content = await fs.readFile(sourcePath, 'utf-8')
697
- const updated = this.updateCommandForGlobalArchitecture(content)
698
-
699
- await fs.writeFile(targetPath, updated, 'utf-8')
700
- copied++
701
- }
176
+ await fs.access(this.claudeCommandsPath)
177
+ const files = await fs.readdir(this.claudeCommandsPath)
178
+ const installedCommands = files.filter(f => f.endsWith('.md')).map(f => f.replace('.md', ''))
702
179
 
703
180
  return {
704
- success: true,
705
- message: `Created ${copied} command templates`,
706
- count: copied,
181
+ installed: installedCommands.length > 0,
182
+ claudeDetected: true,
183
+ commands: installedCommands,
184
+ path: this.claudeCommandsPath,
707
185
  }
708
- } catch (error) {
186
+ } catch {
709
187
  return {
710
- success: false,
711
- message: `Template creation failed: ${error.message}`,
188
+ installed: false,
189
+ claudeDetected: true,
190
+ commands: [],
712
191
  }
713
192
  }
714
193
  }
715
194
 
716
195
  /**
717
- * Generate installation report
718
- * @param {Object} results - Installation results
719
- * @returns {string} Formatted report
196
+ * Update commands (reinstall with latest templates)
197
+ * @returns {Promise<Object>} Update results
720
198
  */
721
- generateReport(results) {
722
- if (!results.success) {
723
- return `❌ Installation failed: ${results.message}`
724
- }
725
-
726
- // Handle single editor installation (installToEditor returns different format)
727
- if (results.editor && !results.editors) {
728
- const lines = [
729
- '✅ Command Installation Complete!',
730
- '',
731
- `📦 Editor: ${results.editor}`,
732
- `📝 Commands installed: ${results.installed}`,
733
- `🔄 Commands updated: ${results.updated}`,
734
- `⊘ Commands skipped: ${results.skipped}`,
735
- '',
736
- '💡 Commands are now available in your editor!',
737
- ]
738
- return lines.join('\n')
739
- }
740
-
741
- // Handle multiple editors installation (installToSelected/installToAll)
742
- const lines = [
743
- '✅ Command Installation Complete!',
744
- '',
745
- `📦 Editors: ${results.editors.join(', ')}`,
746
- `📝 Commands installed: ${results.totalInstalled}`,
747
- `🔄 Commands updated: ${results.totalUpdated}`,
748
- '',
749
- ]
750
-
751
- for (const [key, result] of Object.entries(results.results)) {
752
- if (result.success) {
753
- lines.push(`${this.editors[key].name}:`)
754
- lines.push(` ✓ Installed: ${result.installed}`)
755
- lines.push(` ↻ Updated: ${result.updated}`)
756
- lines.push(` ⊘ Skipped: ${result.skipped}`)
757
- }
758
- }
759
-
760
- lines.push('')
761
- lines.push('💡 Commands are now available in all detected editors!')
762
-
763
- return lines.join('\n')
199
+ async updateCommands() {
200
+ // Simply reinstall - will overwrite with latest templates
201
+ return await this.installCommands()
764
202
  }
765
203
 
766
204
  /**
767
- * Save editor configuration to track installed editors
768
- * @param {string[]} editors - Array of successfully installed editor keys
769
- * @returns {Promise<void>}
205
+ * Get installation path for Claude commands
206
+ * @returns {string} Path to Claude commands directory
770
207
  */
771
- async saveEditorConfig(editors) {
772
- try {
773
- const editorsConfig = require('./editors-config')
774
- const packageJson = require('../package.json')
775
-
776
- // Build paths object
777
- const paths = {}
778
- for (const editorKey of editors) {
779
- const editor = this.editors[editorKey]
780
- if (editor) {
781
- paths[editorKey] = editor.commandsPath
782
- }
783
- }
784
-
785
- await editorsConfig.saveConfig(
786
- editors,
787
- paths,
788
- packageJson.version,
789
- )
790
- } catch (error) {
791
- // Don't fail installation if config save fails
792
- console.error('[command-installer] Error saving editor config:', error.message)
793
- }
794
- }
795
-
796
- /**
797
- * Install Context7 MCP configuration for all detected editors
798
- * @returns {Promise<Object>} Installation results
799
- */
800
- async installContext7MCP() {
801
- const results = {
802
- success: true,
803
- editors: [],
804
- details: {},
805
- }
806
-
807
- const mcpConfigTemplate = path.join(__dirname, '..', 'templates', 'mcp-config.json')
808
- const mcpConfig = JSON.parse(await fs.readFile(mcpConfigTemplate, 'utf-8'))
809
-
810
- try {
811
- // 1. Claude Code: ~/.config/claude/claude_desktop_config.json
812
- if (this.editors.claude.detected) {
813
- const claudeConfigDir = path.join(this.homeDir, '.config', 'claude')
814
- const claudeConfigFile = path.join(claudeConfigDir, 'claude_desktop_config.json')
815
-
816
- await fs.mkdir(claudeConfigDir, { recursive: true })
817
-
818
- let config = {}
819
- if (await this.fileExists(claudeConfigFile)) {
820
- const content = await fs.readFile(claudeConfigFile, 'utf-8')
821
- config = JSON.parse(content)
822
- }
823
-
824
- // Merge Context7 into existing config
825
- config.mcpServers = config.mcpServers || {}
826
- config.mcpServers.context7 = mcpConfig.mcpServers.context7
827
-
828
- await fs.writeFile(claudeConfigFile, JSON.stringify(config, null, 2), 'utf-8')
829
- results.editors.push('Claude Code')
830
- results.details.claude = { success: true, path: claudeConfigFile }
831
- }
832
-
833
- // 2. Cursor: ~/.cursor/mcp.json
834
- if (this.editors.cursor.detected) {
835
- const cursorMcpFile = path.join(this.homeDir, '.cursor', 'mcp.json')
836
- await fs.mkdir(path.dirname(cursorMcpFile), { recursive: true })
837
-
838
- let config = {}
839
- if (await this.fileExists(cursorMcpFile)) {
840
- const content = await fs.readFile(cursorMcpFile, 'utf-8')
841
- config = JSON.parse(content)
842
- }
843
-
844
- config.mcpServers = config.mcpServers || {}
845
- config.mcpServers.context7 = mcpConfig.mcpServers.context7
846
-
847
- await fs.writeFile(cursorMcpFile, JSON.stringify(config, null, 2), 'utf-8')
848
- results.editors.push('Cursor')
849
- results.details.cursor = { success: true, path: cursorMcpFile }
850
- }
851
-
852
- // 3. Windsurf: ~/.windsurf/mcp.json
853
- if (this.editors.windsurf.detected) {
854
- const windsurfMcpFile = path.join(this.homeDir, '.windsurf', 'mcp.json')
855
- await fs.mkdir(path.dirname(windsurfMcpFile), { recursive: true })
856
-
857
- let config = {}
858
- if (await this.fileExists(windsurfMcpFile)) {
859
- const content = await fs.readFile(windsurfMcpFile, 'utf-8')
860
- config = JSON.parse(content)
861
- }
862
-
863
- config.mcpServers = config.mcpServers || {}
864
- config.mcpServers.context7 = mcpConfig.mcpServers.context7
865
-
866
- await fs.writeFile(windsurfMcpFile, JSON.stringify(config, null, 2), 'utf-8')
867
- results.editors.push('Windsurf')
868
- results.details.windsurf = { success: true, path: windsurfMcpFile }
869
- }
870
-
871
- // 4. Codex: Add MCP instructions to ~/.codex/instructions.md
872
- if (this.editors.codex.detected) {
873
- const codexInstructions = this.editors.codex.commandsPath
874
-
875
- let content = ''
876
- if (await this.fileExists(codexInstructions)) {
877
- content = await fs.readFile(codexInstructions, 'utf-8')
878
- }
879
-
880
- // Add MCP section if not present
881
- if (!content.includes('## MCP Integration')) {
882
- const mcpSection = '\n\n## MCP Integration\n\nThe system integrates with MCP servers:\n\n- **Context7**: Library documentation lookup\n- **Filesystem**: Direct file manipulation\n- **Memory**: Persistent decision storage\n- **Sequential**: Deep reasoning for complex problems\n\n### Using Context7\n\nFor any library or framework questions, use Context7 MCP to lookup official documentation:\n\n```\n# Example: Get React hooks documentation\nUse Context7 to lookup React hooks patterns before implementing\n```\n'
883
- content += mcpSection
884
- await fs.writeFile(codexInstructions, content, 'utf-8')
885
- }
886
-
887
- results.editors.push('Codex')
888
- results.details.codex = { success: true, path: codexInstructions }
889
- }
890
-
891
- results.message = `Context7 MCP installed for: ${results.editors.join(', ')}`
892
- } catch (error) {
893
- results.success = false
894
- results.message = `Context7 installation failed: ${error.message}`
895
- }
896
-
897
- return results
208
+ getInstallPath() {
209
+ return this.claudeCommandsPath
898
210
  }
899
211
 
900
212
  /**
901
- * Uninstall commands from a specific editor
902
- * @param {string} editorKey - Editor identifier (claude, cursor, codex, windsurf)
903
- * @returns {Promise<Object>} Uninstallation result
213
+ * Verify command template exists
214
+ * @param {string} commandName - Command name (without .md extension)
215
+ * @returns {Promise<boolean>} True if template exists
904
216
  */
905
- async uninstallFromEditor(editorKey) {
906
- const editor = this.editors[editorKey]
907
-
908
- if (!editor) {
909
- return { success: false, editor: editorKey, message: `Unknown editor: ${editorKey}` }
910
- }
911
-
217
+ async verifyTemplate(commandName) {
912
218
  try {
913
- switch (editor.format) {
914
- case 'slash-commands':
915
- // Remove the /p commands directory
916
- await fs.rm(editor.commandsPath, { recursive: true, force: true })
917
- return {
918
- success: true,
919
- editor: editor.name,
920
- path: editor.commandsPath,
921
- message: `Removed slash commands from ${editor.name}`,
922
- }
923
-
924
- case 'agents-md':
925
- // Remove AGENTS.md if it exists
926
- const exists = await this.fileExists(editor.commandsPath)
927
- if (exists) {
928
- await fs.unlink(editor.commandsPath)
929
- }
930
- return {
931
- success: true,
932
- editor: editor.name,
933
- path: editor.commandsPath,
934
- message: `Removed AGENTS.md from ${editor.name}`,
935
- }
936
-
937
- case 'workflows':
938
- // Remove all p_*.md workflow files
939
- try {
940
- const files = await fs.readdir(editor.commandsPath)
941
- const prjctWorkflows = files.filter(f => f.startsWith('p_') && f.endsWith('.md'))
942
-
943
- for (const file of prjctWorkflows) {
944
- await fs.unlink(path.join(editor.commandsPath, file))
945
- }
946
-
947
- return {
948
- success: true,
949
- editor: editor.name,
950
- path: editor.commandsPath,
951
- removed: prjctWorkflows.length,
952
- message: `Removed ${prjctWorkflows.length} workflows from ${editor.name}`,
953
- }
954
- } catch (error) {
955
- // Directory might not exist, that's ok
956
- return {
957
- success: true,
958
- editor: editor.name,
959
- message: `No workflows found in ${editor.name}`,
960
- }
961
- }
962
-
963
- default:
964
- return {
965
- success: false,
966
- editor: editor.name,
967
- message: `Unknown format: ${editor.format}`,
968
- }
969
- }
970
- } catch (error) {
971
- return {
972
- success: false,
973
- editor: editor.name,
974
- message: `Uninstall failed: ${error.message}`,
975
- }
976
- }
977
- }
978
-
979
- /**
980
- * Uninstall commands from all tracked editors
981
- * @returns {Promise<Object>} Uninstallation results
982
- */
983
- async uninstallFromAll() {
984
- const editorsConfig = require('./editors-config')
985
- const trackedEditors = await editorsConfig.getTrackedEditors()
986
-
987
- if (trackedEditors.length === 0) {
988
- return {
989
- success: true,
990
- message: 'No editors tracked, nothing to uninstall',
991
- results: {},
992
- }
993
- }
994
-
995
- const results = {}
996
- const successfulEditors = []
997
-
998
- for (const editorKey of trackedEditors) {
999
- results[editorKey] = await this.uninstallFromEditor(editorKey)
1000
- if (results[editorKey].success) {
1001
- successfulEditors.push(this.editors[editorKey]?.name || editorKey)
1002
- }
1003
- }
1004
-
1005
- return {
1006
- success: true,
1007
- editors: successfulEditors,
1008
- totalRemoved: successfulEditors.length,
1009
- results,
219
+ const templatePath = path.join(this.templatesDir, `${commandName}.md`)
220
+ await fs.access(templatePath)
221
+ return true
222
+ } catch {
223
+ return false
1010
224
  }
1011
225
  }
1012
226
  }