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,775 @@
|
|
|
1
|
+
const fs = require('fs').promises
|
|
2
|
+
const path = require('path')
|
|
3
|
+
const os = require('os')
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* CommandInstaller - Installs prjct commands to AI editors
|
|
7
|
+
*
|
|
8
|
+
* Handles installation and synchronization of /p:* commands across
|
|
9
|
+
* multiple AI editor environments (Claude Code, Cursor, Codeium, etc.)
|
|
10
|
+
*
|
|
11
|
+
* @version 0.2.1
|
|
12
|
+
*/
|
|
13
|
+
class CommandInstaller {
|
|
14
|
+
constructor() {
|
|
15
|
+
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
|
+
|
|
51
|
+
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
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
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
|
|
69
|
+
*/
|
|
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
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return results
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Get list of command files to install
|
|
99
|
+
* @returns {Promise<string[]>} Array of command filenames
|
|
100
|
+
*/
|
|
101
|
+
async getCommandFiles() {
|
|
102
|
+
try {
|
|
103
|
+
const files = await fs.readdir(this.templatesDir)
|
|
104
|
+
return files.filter(f => f.endsWith('.md'))
|
|
105
|
+
} catch (error) {
|
|
106
|
+
return [
|
|
107
|
+
'init.md',
|
|
108
|
+
'now.md',
|
|
109
|
+
'done.md',
|
|
110
|
+
'ship.md',
|
|
111
|
+
'next.md',
|
|
112
|
+
'idea.md',
|
|
113
|
+
'recap.md',
|
|
114
|
+
'progress.md',
|
|
115
|
+
'stuck.md',
|
|
116
|
+
'context.md',
|
|
117
|
+
'analyze.md',
|
|
118
|
+
'roadmap.md',
|
|
119
|
+
'task.md',
|
|
120
|
+
'git.md',
|
|
121
|
+
'fix.md',
|
|
122
|
+
'test.md',
|
|
123
|
+
]
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
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
|
|
215
|
+
*/
|
|
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
|
+
}
|
|
226
|
+
|
|
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) {
|
|
242
|
+
return {
|
|
243
|
+
success: false,
|
|
244
|
+
message: `Installation failed for ${editor.name}: ${error.message}`,
|
|
245
|
+
}
|
|
246
|
+
}
|
|
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
|
+
|
|
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)
|
|
265
|
+
|
|
266
|
+
const exists = await this.fileExists(targetPath)
|
|
267
|
+
|
|
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)
|
|
278
|
+
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')
|
|
288
|
+
|
|
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()
|
|
329
|
+
|
|
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
|
+
|
|
407
|
+
for (const editorKey of selectedEditors) {
|
|
408
|
+
const editor = this.editors[editorKey]
|
|
409
|
+
|
|
410
|
+
if (!editor) {
|
|
411
|
+
results[editorKey] = {
|
|
412
|
+
success: false,
|
|
413
|
+
message: `Unknown editor: ${editorKey}`,
|
|
414
|
+
}
|
|
415
|
+
continue
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (!editor.detected) {
|
|
419
|
+
results[editorKey] = {
|
|
420
|
+
success: false,
|
|
421
|
+
message: `${editor.name} not detected on this system`,
|
|
422
|
+
}
|
|
423
|
+
continue
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
results[editorKey] = await this.installToEditor(editorKey, forceUpdate)
|
|
427
|
+
if (results[editorKey].success) {
|
|
428
|
+
installedTo.push(editor.name)
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (installedTo.length === 0) {
|
|
433
|
+
return {
|
|
434
|
+
success: false,
|
|
435
|
+
message: 'No editors were successfully installed to',
|
|
436
|
+
results,
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const totalInstalled = Object.values(results)
|
|
441
|
+
.reduce((sum, r) => sum + (r.installed || 0), 0)
|
|
442
|
+
const totalUpdated = Object.values(results)
|
|
443
|
+
.reduce((sum, r) => sum + (r.updated || 0), 0)
|
|
444
|
+
|
|
445
|
+
return {
|
|
446
|
+
success: true,
|
|
447
|
+
editors: installedTo,
|
|
448
|
+
totalInstalled,
|
|
449
|
+
totalUpdated,
|
|
450
|
+
results,
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Install commands to all detected editors
|
|
456
|
+
* @param {boolean} forceUpdate - Force update existing commands
|
|
457
|
+
* @returns {Promise<Object>} Installation results for all editors
|
|
458
|
+
*/
|
|
459
|
+
async installToAll(forceUpdate = false) {
|
|
460
|
+
const detection = await this.detectEditors(this.projectPath)
|
|
461
|
+
const detectedEditors = Object.entries(detection)
|
|
462
|
+
.filter(([_, info]) => info.detected)
|
|
463
|
+
.map(([key, _]) => key)
|
|
464
|
+
|
|
465
|
+
if (detectedEditors.length === 0) {
|
|
466
|
+
return {
|
|
467
|
+
success: false,
|
|
468
|
+
message: 'No AI editors detected',
|
|
469
|
+
results: {},
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
return await this.installToSelected(detectedEditors, forceUpdate)
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Interactive installation with user selection using prompts
|
|
478
|
+
* @param {boolean} forceUpdate - Force update existing commands
|
|
479
|
+
* @returns {Promise<Object>} Installation results
|
|
480
|
+
*/
|
|
481
|
+
async interactiveInstall(forceUpdate = false) {
|
|
482
|
+
const prompts = require('prompts')
|
|
483
|
+
|
|
484
|
+
// Detect all editors
|
|
485
|
+
const detected = await this.detectEditors(this.projectPath)
|
|
486
|
+
|
|
487
|
+
// Create choices for prompts
|
|
488
|
+
const availableEditors = Object.entries(detected)
|
|
489
|
+
.filter(([_, info]) => info.detected)
|
|
490
|
+
.map(([key, info]) => ({
|
|
491
|
+
title: `${this.editors[key].name} (${info.path})`,
|
|
492
|
+
value: key,
|
|
493
|
+
selected: true, // Pre-select all detected editors
|
|
494
|
+
}))
|
|
495
|
+
|
|
496
|
+
if (availableEditors.length === 0) {
|
|
497
|
+
return {
|
|
498
|
+
success: false,
|
|
499
|
+
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)',
|
|
500
|
+
editors: [],
|
|
501
|
+
results: {},
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Show interactive selection prompt
|
|
506
|
+
const response = await prompts({
|
|
507
|
+
type: 'multiselect',
|
|
508
|
+
name: 'selectedEditors',
|
|
509
|
+
message: 'Select AI editors to install commands to:',
|
|
510
|
+
choices: availableEditors,
|
|
511
|
+
min: 1,
|
|
512
|
+
hint: '- Space to select. Return to submit',
|
|
513
|
+
instructions: false,
|
|
514
|
+
})
|
|
515
|
+
|
|
516
|
+
// Check if user cancelled
|
|
517
|
+
if (!response.selectedEditors || response.selectedEditors.length === 0) {
|
|
518
|
+
return {
|
|
519
|
+
success: false,
|
|
520
|
+
message: 'Installation cancelled by user',
|
|
521
|
+
editors: [],
|
|
522
|
+
results: {},
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Install to selected editors
|
|
527
|
+
return await this.installToSelected(response.selectedEditors, forceUpdate)
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Update command content to use global architecture
|
|
532
|
+
* @param {string} content - Original command content
|
|
533
|
+
* @returns {string} Updated content
|
|
534
|
+
*/
|
|
535
|
+
updateCommandForGlobalArchitecture(content) {
|
|
536
|
+
let updated = content.replace(
|
|
537
|
+
/\.prjct\//g,
|
|
538
|
+
'~/.prjct-cli/projects/{id}/',
|
|
539
|
+
)
|
|
540
|
+
|
|
541
|
+
if (!content.includes('Global Architecture')) {
|
|
542
|
+
const frontmatter = content.match(/^---[\s\S]*?---/m)
|
|
543
|
+
if (frontmatter) {
|
|
544
|
+
const note = `
|
|
545
|
+
|
|
546
|
+
## Global Architecture
|
|
547
|
+
This command uses the global prjct architecture:
|
|
548
|
+
- Data stored in: \`~/.prjct-cli/projects/{id}/\`
|
|
549
|
+
- Config stored in: \`{project}/.prjct/prjct.config.json\`
|
|
550
|
+
- Commands synchronized across all editors
|
|
551
|
+
|
|
552
|
+
`
|
|
553
|
+
updated = content.replace(frontmatter[0], frontmatter[0] + note)
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
return updated
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Check if a file exists
|
|
562
|
+
* @param {string} filePath - Path to check
|
|
563
|
+
* @returns {Promise<boolean>} True if file exists
|
|
564
|
+
*/
|
|
565
|
+
async fileExists(filePath) {
|
|
566
|
+
try {
|
|
567
|
+
await fs.access(filePath)
|
|
568
|
+
return true
|
|
569
|
+
} catch {
|
|
570
|
+
return false
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* Create command templates directory and copy existing commands
|
|
576
|
+
* @returns {Promise<Object>} Template creation result
|
|
577
|
+
*/
|
|
578
|
+
async createTemplates() {
|
|
579
|
+
try {
|
|
580
|
+
await fs.mkdir(this.templatesDir, { recursive: true })
|
|
581
|
+
|
|
582
|
+
const claudeCommandsPath = this.editors.claude.commandsPath
|
|
583
|
+
const hasClaudeCommands = await this.fileExists(claudeCommandsPath)
|
|
584
|
+
|
|
585
|
+
if (!hasClaudeCommands) {
|
|
586
|
+
return {
|
|
587
|
+
success: false,
|
|
588
|
+
message: 'No source commands found. Claude Code commands directory not detected.',
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
const files = await fs.readdir(claudeCommandsPath)
|
|
593
|
+
const mdFiles = files.filter(f => f.endsWith('.md'))
|
|
594
|
+
|
|
595
|
+
let copied = 0
|
|
596
|
+
for (const filename of mdFiles) {
|
|
597
|
+
const sourcePath = path.join(claudeCommandsPath, filename)
|
|
598
|
+
const targetPath = path.join(this.templatesDir, filename)
|
|
599
|
+
|
|
600
|
+
const content = await fs.readFile(sourcePath, 'utf-8')
|
|
601
|
+
const updated = this.updateCommandForGlobalArchitecture(content)
|
|
602
|
+
|
|
603
|
+
await fs.writeFile(targetPath, updated, 'utf-8')
|
|
604
|
+
copied++
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
return {
|
|
608
|
+
success: true,
|
|
609
|
+
message: `Created ${copied} command templates`,
|
|
610
|
+
count: copied,
|
|
611
|
+
}
|
|
612
|
+
} catch (error) {
|
|
613
|
+
return {
|
|
614
|
+
success: false,
|
|
615
|
+
message: `Template creation failed: ${error.message}`,
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* Generate installation report
|
|
622
|
+
* @param {Object} results - Installation results
|
|
623
|
+
* @returns {string} Formatted report
|
|
624
|
+
*/
|
|
625
|
+
generateReport(results) {
|
|
626
|
+
if (!results.success) {
|
|
627
|
+
return `❌ Installation failed: ${results.message}`
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// Handle single editor installation (installToEditor returns different format)
|
|
631
|
+
if (results.editor && !results.editors) {
|
|
632
|
+
const lines = [
|
|
633
|
+
'✅ Command Installation Complete!',
|
|
634
|
+
'',
|
|
635
|
+
`📦 Editor: ${results.editor}`,
|
|
636
|
+
`📝 Commands installed: ${results.installed}`,
|
|
637
|
+
`🔄 Commands updated: ${results.updated}`,
|
|
638
|
+
`⊘ Commands skipped: ${results.skipped}`,
|
|
639
|
+
'',
|
|
640
|
+
'💡 Commands are now available in your editor!',
|
|
641
|
+
]
|
|
642
|
+
return lines.join('\n')
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// Handle multiple editors installation (installToSelected/installToAll)
|
|
646
|
+
const lines = [
|
|
647
|
+
'✅ Command Installation Complete!',
|
|
648
|
+
'',
|
|
649
|
+
`📦 Editors: ${results.editors.join(', ')}`,
|
|
650
|
+
`📝 Commands installed: ${results.totalInstalled}`,
|
|
651
|
+
`🔄 Commands updated: ${results.totalUpdated}`,
|
|
652
|
+
'',
|
|
653
|
+
]
|
|
654
|
+
|
|
655
|
+
for (const [key, result] of Object.entries(results.results)) {
|
|
656
|
+
if (result.success) {
|
|
657
|
+
lines.push(`${this.editors[key].name}:`)
|
|
658
|
+
lines.push(` ✓ Installed: ${result.installed}`)
|
|
659
|
+
lines.push(` ↻ Updated: ${result.updated}`)
|
|
660
|
+
lines.push(` ⊘ Skipped: ${result.skipped}`)
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
lines.push('')
|
|
665
|
+
lines.push('💡 Commands are now available in all detected editors!')
|
|
666
|
+
|
|
667
|
+
return lines.join('\n')
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* Install Context7 MCP configuration for all detected editors
|
|
672
|
+
* @returns {Promise<Object>} Installation results
|
|
673
|
+
*/
|
|
674
|
+
async installContext7MCP() {
|
|
675
|
+
const results = {
|
|
676
|
+
success: true,
|
|
677
|
+
editors: [],
|
|
678
|
+
details: {},
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
const mcpConfigTemplate = path.join(__dirname, '..', 'templates', 'mcp-config.json')
|
|
682
|
+
const mcpConfig = JSON.parse(await fs.readFile(mcpConfigTemplate, 'utf-8'))
|
|
683
|
+
|
|
684
|
+
try {
|
|
685
|
+
// 1. Claude Code: ~/.config/claude/claude_desktop_config.json
|
|
686
|
+
if (this.editors.claude.detected) {
|
|
687
|
+
const claudeConfigDir = path.join(this.homeDir, '.config', 'claude')
|
|
688
|
+
const claudeConfigFile = path.join(claudeConfigDir, 'claude_desktop_config.json')
|
|
689
|
+
|
|
690
|
+
await fs.mkdir(claudeConfigDir, { recursive: true })
|
|
691
|
+
|
|
692
|
+
let config = {}
|
|
693
|
+
if (await this.fileExists(claudeConfigFile)) {
|
|
694
|
+
const content = await fs.readFile(claudeConfigFile, 'utf-8')
|
|
695
|
+
config = JSON.parse(content)
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// Merge Context7 into existing config
|
|
699
|
+
config.mcpServers = config.mcpServers || {}
|
|
700
|
+
config.mcpServers.context7 = mcpConfig.mcpServers.context7
|
|
701
|
+
|
|
702
|
+
await fs.writeFile(claudeConfigFile, JSON.stringify(config, null, 2), 'utf-8')
|
|
703
|
+
results.editors.push('Claude Code')
|
|
704
|
+
results.details.claude = { success: true, path: claudeConfigFile }
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// 2. Cursor: ~/.cursor/mcp.json
|
|
708
|
+
if (this.editors.cursor.detected) {
|
|
709
|
+
const cursorMcpFile = path.join(this.homeDir, '.cursor', 'mcp.json')
|
|
710
|
+
await fs.mkdir(path.dirname(cursorMcpFile), { recursive: true })
|
|
711
|
+
|
|
712
|
+
let config = {}
|
|
713
|
+
if (await this.fileExists(cursorMcpFile)) {
|
|
714
|
+
const content = await fs.readFile(cursorMcpFile, 'utf-8')
|
|
715
|
+
config = JSON.parse(content)
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
config.mcpServers = config.mcpServers || {}
|
|
719
|
+
config.mcpServers.context7 = mcpConfig.mcpServers.context7
|
|
720
|
+
|
|
721
|
+
await fs.writeFile(cursorMcpFile, JSON.stringify(config, null, 2), 'utf-8')
|
|
722
|
+
results.editors.push('Cursor')
|
|
723
|
+
results.details.cursor = { success: true, path: cursorMcpFile }
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// 3. Windsurf: ~/.windsurf/mcp.json
|
|
727
|
+
if (this.editors.windsurf.detected) {
|
|
728
|
+
const windsurfMcpFile = path.join(this.homeDir, '.windsurf', 'mcp.json')
|
|
729
|
+
await fs.mkdir(path.dirname(windsurfMcpFile), { recursive: true })
|
|
730
|
+
|
|
731
|
+
let config = {}
|
|
732
|
+
if (await this.fileExists(windsurfMcpFile)) {
|
|
733
|
+
const content = await fs.readFile(windsurfMcpFile, 'utf-8')
|
|
734
|
+
config = JSON.parse(content)
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
config.mcpServers = config.mcpServers || {}
|
|
738
|
+
config.mcpServers.context7 = mcpConfig.mcpServers.context7
|
|
739
|
+
|
|
740
|
+
await fs.writeFile(windsurfMcpFile, JSON.stringify(config, null, 2), 'utf-8')
|
|
741
|
+
results.editors.push('Windsurf')
|
|
742
|
+
results.details.windsurf = { success: true, path: windsurfMcpFile }
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// 4. Codex: Add MCP instructions to ~/.codex/instructions.md
|
|
746
|
+
if (this.editors.codex.detected) {
|
|
747
|
+
const codexInstructions = this.editors.codex.commandsPath
|
|
748
|
+
|
|
749
|
+
let content = ''
|
|
750
|
+
if (await this.fileExists(codexInstructions)) {
|
|
751
|
+
content = await fs.readFile(codexInstructions, 'utf-8')
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// Add MCP section if not present
|
|
755
|
+
if (!content.includes('## MCP Integration')) {
|
|
756
|
+
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'
|
|
757
|
+
content += mcpSection
|
|
758
|
+
await fs.writeFile(codexInstructions, content, 'utf-8')
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
results.editors.push('Codex')
|
|
762
|
+
results.details.codex = { success: true, path: codexInstructions }
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
results.message = `Context7 MCP installed for: ${results.editors.join(', ')}`
|
|
766
|
+
} catch (error) {
|
|
767
|
+
results.success = false
|
|
768
|
+
results.message = `Context7 installation failed: ${error.message}`
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
return results
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
module.exports = new CommandInstaller()
|