prjct-cli 0.35.4 → 0.36.1

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/core/index.ts CHANGED
@@ -9,6 +9,10 @@ import { commandRegistry } from './commands/registry'
9
9
  import './commands/register' // Ensure commands are registered
10
10
  import out from './utils/output'
11
11
  import type { CommandMeta } from './commands/registry'
12
+ import { detectAllProviders, Providers } from './infrastructure/ai-provider'
13
+ import fs from 'fs'
14
+ import path from 'path'
15
+ import os from 'os'
12
16
 
13
17
  interface ParsedCommandArgs {
14
18
  parsedArgs: string[]
@@ -27,7 +31,7 @@ async function main(): Promise<void> {
27
31
 
28
32
  if (['-v', '--version', 'version'].includes(commandName)) {
29
33
  const packageJson = await import('../package.json')
30
- console.log(`prjct-cli v${packageJson.version}`)
34
+ displayVersion(packageJson.version)
31
35
  process.exit(0)
32
36
  }
33
37
 
@@ -175,44 +179,106 @@ function parseCommandArgs(cmd: CommandMeta, rawArgs: string[]): ParsedCommandArg
175
179
  return { parsedArgs, options }
176
180
  }
177
181
 
182
+ // Colors for version display
183
+ const CYAN = '\x1b[36m'
184
+ const GREEN = '\x1b[32m'
185
+ const YELLOW = '\x1b[33m'
186
+ const DIM = '\x1b[2m'
187
+ const RESET = '\x1b[0m'
188
+
178
189
  /**
179
- * Display help using registry
190
+ * Display version with provider status
180
191
  */
181
- function displayHelp(): void {
182
- const categories = commandRegistry.getAllCategories()
183
- const categorizedCommands: Record<string, CommandMeta[]> = {}
192
+ function displayVersion(version: string): void {
193
+ const detection = detectAllProviders()
194
+
195
+ // Check if prjct commands are installed for each provider
196
+ const claudeCommandPath = path.join(os.homedir(), '.claude', 'commands', 'p.md')
197
+ const geminiCommandPath = path.join(os.homedir(), '.gemini', 'commands', 'p.toml')
198
+ const claudeConfigured = fs.existsSync(claudeCommandPath)
199
+ const geminiConfigured = fs.existsSync(geminiCommandPath)
200
+
201
+ console.log(`
202
+ ${CYAN}p/${RESET} prjct v${version}
203
+ ${DIM}Context layer for AI coding agents${RESET}
204
+
205
+ ${DIM}Providers:${RESET}`)
206
+
207
+ // Claude status
208
+ if (detection.claude.installed) {
209
+ const status = claudeConfigured ? `${GREEN}✓ ready${RESET}` : `${YELLOW}● installed${RESET}`
210
+ const ver = detection.claude.version ? ` (v${detection.claude.version})` : ''
211
+ console.log(` Claude Code ${status}${DIM}${ver}${RESET}`)
212
+ } else {
213
+ console.log(` Claude Code ${DIM}○ not installed${RESET}`)
214
+ }
184
215
 
185
- // Group commands by category (exclude deprecated)
186
- commandRegistry.getTerminalCommands().forEach((cmd) => {
187
- if (cmd.deprecated) return
216
+ // Gemini status
217
+ if (detection.gemini.installed) {
218
+ const status = geminiConfigured ? `${GREEN}✓ ready${RESET}` : `${YELLOW}● installed${RESET}`
219
+ const ver = detection.gemini.version ? ` (v${detection.gemini.version})` : ''
220
+ console.log(` Gemini CLI ${status}${DIM}${ver}${RESET}`)
221
+ } else {
222
+ console.log(` Gemini CLI ${DIM}○ not installed${RESET}`)
223
+ }
188
224
 
189
- if (!categorizedCommands[cmd.group]) {
190
- categorizedCommands[cmd.group] = []
191
- }
192
- categorizedCommands[cmd.group].push(cmd)
193
- })
194
-
195
- console.log('prjct - Developer momentum tool for solo builders')
196
- console.log('\nAvailable commands:\n')
197
-
198
- // Display commands by category
199
- Object.entries(categorizedCommands).forEach(([categoryKey, cmds]) => {
200
- const categoryInfo = categories.get(categoryKey)
201
- console.log(` ${categoryInfo?.title || categoryKey}:`)
202
-
203
- cmds.forEach((cmd) => {
204
- const params = cmd.params ? ` ${cmd.params}` : ''
205
- const spacing = ' '.repeat(Math.max(20 - cmd.name.length - params.length, 1))
206
- const impl = cmd.implemented ? '' : ' (not implemented)'
207
- console.log(` ${cmd.name}${params}${spacing}${cmd.description}${impl}`)
208
- })
209
-
210
- console.log('')
211
- })
212
-
213
- const stats = commandRegistry.getStats()
214
- console.log(`Total: ${stats.implemented} implemented / ${stats.total} commands`)
215
- console.log('\nFor more info: https://prjct.app')
225
+ console.log(`
226
+ ${DIM}Run 'prjct start' to configure providers${RESET}
227
+ ${CYAN}https://prjct.app${RESET}
228
+ `)
229
+ }
230
+
231
+ /**
232
+ * Display help using registry
233
+ */
234
+ function displayHelp(): void {
235
+ console.log(`
236
+ prjct - Context layer for AI coding agents
237
+ Works with Claude Code, Gemini CLI, and more.
238
+
239
+ QUICK START
240
+ -----------
241
+ 1. prjct start Configure your AI provider (Claude/Gemini)
242
+ 2. Open project in your AI coding agent
243
+ 3. Type: p. sync Analyze project and generate context
244
+ 4. Type: p. task "..." Start working on a task
245
+
246
+ HOW IT WORKS
247
+ ------------
248
+ prjct gives AI agents the context they need about your project.
249
+ Use "p." commands inside Claude Code or Gemini CLI:
250
+
251
+ p. sync Analyze project, generate domain agents
252
+ p. task "desc" Start task with auto-classification
253
+ p. done Complete current subtask
254
+ p. ship "name" Ship feature with PR + version
255
+
256
+ TERMINAL COMMANDS (this CLI)
257
+ ----------------------------
258
+ prjct start First-time setup
259
+ prjct setup Reconfigure installations
260
+ prjct init Initialize project (creates .prjct/)
261
+ prjct sync Sync project state
262
+
263
+ EXAMPLES
264
+ --------
265
+ # First time setup
266
+ $ prjct start
267
+
268
+ # Initialize a new project
269
+ $ cd my-project && prjct init
270
+
271
+ # Inside Claude Code or Gemini CLI
272
+ > p. sync
273
+ > p. task "add user authentication"
274
+ > p. done
275
+ > p. ship "user auth"
276
+
277
+ MORE INFO
278
+ ---------
279
+ Documentation: https://prjct.app
280
+ GitHub: https://github.com/jlopezlira/prjct-cli
281
+ `)
216
282
  }
217
283
 
218
284
  // Run CLI
@@ -0,0 +1,312 @@
1
+ /**
2
+ * AI Provider - Multi-agent support for prjct-cli
3
+ *
4
+ * Supports both Claude Code and Gemini CLI with a unified abstraction layer.
5
+ * Both agents share similar architectures:
6
+ * - Context files: CLAUDE.md / GEMINI.md
7
+ * - Skills: Both use SKILL.md format (identical!)
8
+ * - Commands: .md (Claude) / .toml (Gemini)
9
+ * - MCP: Both support Model Context Protocol
10
+ *
11
+ * @see https://geminicli.com/docs/cli/gemini-md/
12
+ * @see https://geminicli.com/docs/cli/skills/
13
+ */
14
+
15
+ import { execSync } from 'child_process'
16
+ import fs from 'fs'
17
+ import path from 'path'
18
+ import os from 'os'
19
+ import type {
20
+ AIProviderName,
21
+ AIProviderConfig,
22
+ ProviderDetectionResult,
23
+ ProviderSelectionResult,
24
+ ProviderBranding,
25
+ } from '../types/provider'
26
+
27
+ // =============================================================================
28
+ // Provider Configurations
29
+ // =============================================================================
30
+
31
+ /**
32
+ * Claude Code provider configuration
33
+ */
34
+ export const ClaudeProvider: AIProviderConfig = {
35
+ name: 'claude',
36
+ displayName: 'Claude Code',
37
+ cliCommand: 'claude',
38
+ configDir: path.join(os.homedir(), '.claude'),
39
+ contextFile: 'CLAUDE.md',
40
+ skillsDir: path.join(os.homedir(), '.claude', 'skills'),
41
+ commandsDir: '.claude/commands',
42
+ commandFormat: 'md',
43
+ settingsFile: 'settings.json',
44
+ projectSettingsFile: 'settings.local.json',
45
+ ignoreFile: '.claudeignore',
46
+ websiteUrl: 'https://www.anthropic.com/claude',
47
+ docsUrl: 'https://docs.anthropic.com/claude-code',
48
+ }
49
+
50
+ /**
51
+ * Gemini CLI provider configuration
52
+ */
53
+ export const GeminiProvider: AIProviderConfig = {
54
+ name: 'gemini',
55
+ displayName: 'Gemini CLI',
56
+ cliCommand: 'gemini',
57
+ configDir: path.join(os.homedir(), '.gemini'),
58
+ contextFile: 'GEMINI.md',
59
+ skillsDir: path.join(os.homedir(), '.gemini', 'skills'),
60
+ commandsDir: '.gemini/commands',
61
+ commandFormat: 'toml',
62
+ settingsFile: 'settings.json',
63
+ projectSettingsFile: 'settings.json',
64
+ ignoreFile: '.geminiignore',
65
+ websiteUrl: 'https://geminicli.com',
66
+ docsUrl: 'https://geminicli.com/docs',
67
+ }
68
+
69
+ /**
70
+ * All available providers
71
+ */
72
+ export const Providers: Record<AIProviderName, AIProviderConfig> = {
73
+ claude: ClaudeProvider,
74
+ gemini: GeminiProvider,
75
+ }
76
+
77
+ // =============================================================================
78
+ // Provider Detection
79
+ // =============================================================================
80
+
81
+ /**
82
+ * Check if a CLI command is available
83
+ */
84
+ function whichCommand(command: string): string | null {
85
+ try {
86
+ const result = execSync(`which ${command}`, { stdio: 'pipe', encoding: 'utf-8' })
87
+ return result.trim()
88
+ } catch {
89
+ return null
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Get CLI version
95
+ */
96
+ function getCliVersion(command: string): string | null {
97
+ try {
98
+ const result = execSync(`${command} --version`, { stdio: 'pipe', encoding: 'utf-8' })
99
+ // Extract version number from output (e.g., "claude 1.0.0" -> "1.0.0")
100
+ const match = result.match(/\d+\.\d+\.\d+/)
101
+ return match ? match[0] : result.trim()
102
+ } catch {
103
+ return null
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Detect if a specific provider is installed
109
+ */
110
+ export function detectProvider(provider: AIProviderName): ProviderDetectionResult {
111
+ const config = Providers[provider]
112
+ const cliPath = whichCommand(config.cliCommand)
113
+
114
+ if (!cliPath) {
115
+ return { installed: false }
116
+ }
117
+
118
+ const version = getCliVersion(config.cliCommand)
119
+
120
+ return {
121
+ installed: true,
122
+ version: version || undefined,
123
+ path: cliPath,
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Detect all available providers
129
+ */
130
+ export function detectAllProviders(): Record<AIProviderName, ProviderDetectionResult> {
131
+ return {
132
+ claude: detectProvider('claude'),
133
+ gemini: detectProvider('gemini'),
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Get the active provider based on detection or configuration
139
+ *
140
+ * Priority:
141
+ * 1. Check project config for saved provider preference
142
+ * 2. Auto-detect single installed provider
143
+ * 3. Default to Claude if both installed (backward compatibility)
144
+ */
145
+ export function getActiveProvider(projectProvider?: AIProviderName): AIProviderConfig {
146
+ // If project has a saved preference, use it
147
+ if (projectProvider && Providers[projectProvider]) {
148
+ return Providers[projectProvider]
149
+ }
150
+
151
+ // Auto-detect
152
+ const detection = detectAllProviders()
153
+
154
+ // If only one is installed, use it
155
+ if (detection.claude.installed && !detection.gemini.installed) {
156
+ return ClaudeProvider
157
+ }
158
+ if (detection.gemini.installed && !detection.claude.installed) {
159
+ return GeminiProvider
160
+ }
161
+
162
+ // Default to Claude for backward compatibility
163
+ return ClaudeProvider
164
+ }
165
+
166
+ /**
167
+ * Check if config directory exists for a provider
168
+ */
169
+ export function hasProviderConfig(provider: AIProviderName): boolean {
170
+ const config = Providers[provider]
171
+ return fs.existsSync(config.configDir)
172
+ }
173
+
174
+ // =============================================================================
175
+ // Provider Branding
176
+ // =============================================================================
177
+
178
+ /**
179
+ * Get provider-specific branding
180
+ */
181
+ export function getProviderBranding(provider: AIProviderName): ProviderBranding {
182
+ const config = Providers[provider]
183
+
184
+ if (provider === 'gemini') {
185
+ return {
186
+ commitFooter: `🤖 Generated with [p/](https://www.prjct.app/)
187
+ Designed for [Gemini](${config.websiteUrl})`,
188
+ signature: '⚡ prjct + Gemini',
189
+ }
190
+ }
191
+
192
+ // Default: Claude
193
+ return {
194
+ commitFooter: `🤖 Generated with [p/](https://www.prjct.app/)
195
+ Designed for [Claude](${config.websiteUrl})`,
196
+ signature: '⚡ prjct + Claude',
197
+ }
198
+ }
199
+
200
+ // =============================================================================
201
+ // Provider Paths
202
+ // =============================================================================
203
+
204
+ /**
205
+ * Get full path to global context file
206
+ */
207
+ export function getGlobalContextPath(provider: AIProviderName): string {
208
+ const config = Providers[provider]
209
+ return path.join(config.configDir, config.contextFile)
210
+ }
211
+
212
+ /**
213
+ * Get full path to global settings file
214
+ */
215
+ export function getGlobalSettingsPath(provider: AIProviderName): string {
216
+ const config = Providers[provider]
217
+ return path.join(config.configDir, config.settingsFile)
218
+ }
219
+
220
+ /**
221
+ * Get full path to skills directory
222
+ */
223
+ export function getSkillsPath(provider: AIProviderName): string {
224
+ return Providers[provider].skillsDir
225
+ }
226
+
227
+ /**
228
+ * Get commands directory relative to project root
229
+ */
230
+ export function getCommandsDir(provider: AIProviderName): string {
231
+ return Providers[provider].commandsDir
232
+ }
233
+
234
+ /**
235
+ * Get full path to commands directory in a project
236
+ */
237
+ export function getProjectCommandsPath(provider: AIProviderName, projectRoot: string): string {
238
+ const config = Providers[provider]
239
+ return path.join(projectRoot, config.commandsDir)
240
+ }
241
+
242
+ // =============================================================================
243
+ // Provider Selection (for setup)
244
+ // =============================================================================
245
+
246
+ /**
247
+ * Determine which provider to use during setup
248
+ * Returns selection result with detection details
249
+ */
250
+ export function selectProvider(): ProviderSelectionResult {
251
+ const detection = detectAllProviders()
252
+
253
+ const claudeInstalled = detection.claude.installed
254
+ const geminiInstalled = detection.gemini.installed
255
+
256
+ // Neither installed
257
+ if (!claudeInstalled && !geminiInstalled) {
258
+ // Default to Claude, setup will prompt to install
259
+ return {
260
+ provider: 'claude',
261
+ userSelected: false,
262
+ detection,
263
+ }
264
+ }
265
+
266
+ // Only Claude installed
267
+ if (claudeInstalled && !geminiInstalled) {
268
+ return {
269
+ provider: 'claude',
270
+ userSelected: false,
271
+ detection,
272
+ }
273
+ }
274
+
275
+ // Only Gemini installed
276
+ if (geminiInstalled && !claudeInstalled) {
277
+ return {
278
+ provider: 'gemini',
279
+ userSelected: false,
280
+ detection,
281
+ }
282
+ }
283
+
284
+ // Both installed - will need user selection
285
+ // For now, default to Claude (caller should prompt user)
286
+ return {
287
+ provider: 'claude',
288
+ userSelected: true, // Indicates user should be prompted
289
+ detection,
290
+ }
291
+ }
292
+
293
+ // =============================================================================
294
+ // Exports
295
+ // =============================================================================
296
+
297
+ export default {
298
+ Providers,
299
+ ClaudeProvider,
300
+ GeminiProvider,
301
+ detectProvider,
302
+ detectAllProviders,
303
+ getActiveProvider,
304
+ hasProviderConfig,
305
+ getProviderBranding,
306
+ getGlobalContextPath,
307
+ getGlobalSettingsPath,
308
+ getSkillsPath,
309
+ getCommandsDir,
310
+ getProjectCommandsPath,
311
+ selectProvider,
312
+ }
@@ -1,12 +1,15 @@
1
1
  /**
2
2
  * Command Installer
3
- * Installs prjct commands in Claude Code and other editors.
3
+ * Installs prjct commands in Claude Code and other AI CLI agents.
4
4
  *
5
- * 100% Claude-focused architecture
6
- * Handles installation and synchronization of /p:* commands
7
- * to Claude's native slash command system
5
+ * Architecture:
6
+ * - Claude: Full command sync to ~/.claude/commands/p/ (workaround for bug #2422)
7
+ * - Gemini: Simple router (p.toml) to ~/.gemini/commands/ (handled by setup.ts)
8
8
  *
9
- * @version 0.5.0
9
+ * This module handles the more complex Claude installation.
10
+ * For Gemini, see setup.ts::installGeminiRouter()
11
+ *
12
+ * @version 0.6.0 - Multi-provider support
10
13
  */
11
14
 
12
15
  import fs from 'fs/promises'
@@ -522,6 +525,47 @@ export class CommandInstaller {
522
525
  }
523
526
  }
524
527
 
528
+ // =============================================================================
529
+ // Multi-Provider Support
530
+ // =============================================================================
531
+
532
+ /**
533
+ * Get installation paths for all providers
534
+ */
535
+ export function getProviderPaths(): {
536
+ claude: { commands: string; config: string; router: string }
537
+ gemini: { commands: string; config: string; router: string }
538
+ } {
539
+ const homeDir = os.homedir()
540
+ return {
541
+ claude: {
542
+ commands: path.join(homeDir, '.claude', 'commands', 'p'),
543
+ config: path.join(homeDir, '.claude'),
544
+ router: path.join(homeDir, '.claude', 'commands', 'p.md'),
545
+ },
546
+ gemini: {
547
+ commands: path.join(homeDir, '.gemini', 'commands'),
548
+ config: path.join(homeDir, '.gemini'),
549
+ router: path.join(homeDir, '.gemini', 'commands', 'p.toml'),
550
+ },
551
+ }
552
+ }
553
+
554
+ /**
555
+ * Check if provider router is installed
556
+ */
557
+ export async function isRouterInstalled(provider: 'claude' | 'gemini'): Promise<boolean> {
558
+ const paths = getProviderPaths()
559
+ const routerPath = paths[provider].router
560
+
561
+ try {
562
+ await fs.access(routerPath)
563
+ return true
564
+ } catch {
565
+ return false
566
+ }
567
+ }
568
+
525
569
  // =============================================================================
526
570
  // Exports
527
571
  // =============================================================================
@@ -1,21 +1,25 @@
1
1
  /**
2
- * EditorsConfig - Manages Claude installation tracking
2
+ * EditorsConfig - Manages AI CLI installation tracking
3
3
  *
4
- * Tracks prjct commands installation in Claude (Code + Desktop),
4
+ * Tracks prjct commands installation in AI CLIs (Claude Code, Gemini CLI),
5
5
  * enabling automatic updates when npm package is updated.
6
6
  *
7
7
  * Config location: ~/.prjct-cli/config/installed-editors.json
8
8
  *
9
- * @version 0.5.0
9
+ * @version 0.6.0
10
10
  */
11
11
 
12
12
  import fs from 'fs/promises'
13
13
  import path from 'path'
14
14
  import os from 'os'
15
+ import type { AIProviderName } from '../types/provider'
15
16
 
16
17
  interface EditorConfig {
17
18
  version: string
19
+ /** @deprecated Use 'provider' instead */
18
20
  editor: string
21
+ /** AI provider name (claude or gemini) */
22
+ provider: AIProviderName
19
23
  lastInstall: string
20
24
  path: string
21
25
  }
@@ -61,15 +65,16 @@ class EditorsConfig {
61
65
  /**
62
66
  * Save installation configuration
63
67
  */
64
- async saveConfig(version: string, claudePath: string): Promise<boolean> {
68
+ async saveConfig(version: string, installPath: string, provider: AIProviderName = 'claude'): Promise<boolean> {
65
69
  try {
66
70
  await this.ensureConfigDir()
67
71
 
68
72
  const config: EditorConfig = {
69
73
  version,
70
- editor: 'claude',
74
+ editor: provider, // deprecated, kept for backward compatibility
75
+ provider,
71
76
  lastInstall: new Date().toISOString(),
72
- path: claudePath,
77
+ path: installPath,
73
78
  }
74
79
 
75
80
  await fs.writeFile(this.configFile, JSON.stringify(config, null, 2), 'utf-8')
@@ -81,6 +86,15 @@ class EditorsConfig {
81
86
  }
82
87
  }
83
88
 
89
+ /**
90
+ * Get the configured provider
91
+ */
92
+ async getProvider(): Promise<AIProviderName | null> {
93
+ const config = await this.loadConfig()
94
+ if (!config) return null
95
+ return config.provider || (config.editor as AIProviderName) || 'claude'
96
+ }
97
+
84
98
  /**
85
99
  * Get last installed version
86
100
  */