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/CHANGELOG.md +43 -0
- package/README.md +63 -618
- package/bin/prjct.ts +116 -17
- package/core/cli/start.ts +387 -0
- package/core/index.ts +101 -35
- package/core/infrastructure/ai-provider.ts +312 -0
- package/core/infrastructure/command-installer.ts +49 -5
- package/core/infrastructure/editors-config.ts +20 -6
- package/core/infrastructure/setup.ts +227 -62
- package/core/services/skill-service.ts +52 -16
- package/core/types/index.ts +12 -0
- package/core/types/provider.ts +110 -0
- package/core/utils/branding.ts +20 -3
- package/dist/bin/prjct.mjs +1482 -2763
- package/dist/core/infrastructure/command-installer.js +33 -2
- package/dist/core/infrastructure/editors-config.js +13 -3
- package/dist/core/infrastructure/setup.js +293 -73
- package/package.json +9 -9
- package/scripts/postinstall.js +17 -119
- package/templates/agents/AGENTS.md +9 -1
- package/templates/commands/p.md +1 -1
- package/templates/commands/p.toml +37 -0
- package/templates/global/CLAUDE.md +33 -1
- package/templates/global/GEMINI.md +265 -0
- package/templates/global/STORAGE-SPEC.md +256 -0
- package/templates/global/docs/agents.md +88 -0
- package/templates/global/docs/architecture.md +103 -0
- package/templates/global/docs/commands.md +96 -0
- package/templates/global/docs/validation.md +95 -0
|
@@ -2,10 +2,15 @@
|
|
|
2
2
|
* Setup Module - Core installation logic
|
|
3
3
|
*
|
|
4
4
|
* Executes ALL setup needed for prjct-cli:
|
|
5
|
-
* 1.
|
|
6
|
-
* 2.
|
|
7
|
-
* 3.
|
|
8
|
-
* 4.
|
|
5
|
+
* 1. Detect AI provider (Claude Code or Gemini CLI)
|
|
6
|
+
* 2. Install CLI if missing
|
|
7
|
+
* 3. Sync commands to provider's commands directory
|
|
8
|
+
* 4. Install global config (CLAUDE.md or GEMINI.md)
|
|
9
|
+
* 5. Save version in editors-config
|
|
10
|
+
*
|
|
11
|
+
* Supports multiple AI CLI agents:
|
|
12
|
+
* - Claude Code: ~/.claude/commands/p/, CLAUDE.md
|
|
13
|
+
* - Gemini CLI: ~/.gemini/commands/p/, GEMINI.md
|
|
9
14
|
*
|
|
10
15
|
* This module is called from:
|
|
11
16
|
* - core/index.js (on first CLI use)
|
|
@@ -20,6 +25,13 @@ import installer from './command-installer'
|
|
|
20
25
|
import editorsConfig from './editors-config'
|
|
21
26
|
import { VERSION, getPackageRoot } from '../utils/version'
|
|
22
27
|
import { isNotFoundError } from '../types/fs'
|
|
28
|
+
import {
|
|
29
|
+
selectProvider,
|
|
30
|
+
detectProvider,
|
|
31
|
+
detectAllProviders,
|
|
32
|
+
Providers,
|
|
33
|
+
} from './ai-provider'
|
|
34
|
+
import type { AIProviderName, AIProviderConfig } from '../types/provider'
|
|
23
35
|
|
|
24
36
|
// Colors
|
|
25
37
|
const GREEN = '\x1b[32m'
|
|
@@ -27,102 +39,161 @@ const YELLOW = '\x1b[33m'
|
|
|
27
39
|
const DIM = '\x1b[2m'
|
|
28
40
|
const NC = '\x1b[0m'
|
|
29
41
|
|
|
42
|
+
interface ProviderSetupResult {
|
|
43
|
+
provider: AIProviderName
|
|
44
|
+
cliInstalled: boolean
|
|
45
|
+
commandsAdded: number
|
|
46
|
+
commandsUpdated: number
|
|
47
|
+
configAction: string | null
|
|
48
|
+
}
|
|
49
|
+
|
|
30
50
|
interface SetupResults {
|
|
31
|
-
|
|
51
|
+
provider: AIProviderName // Primary provider (for backward compat)
|
|
52
|
+
providers: ProviderSetupResult[] // All installed providers
|
|
53
|
+
cliInstalled: boolean
|
|
32
54
|
commandsAdded: number
|
|
33
55
|
commandsUpdated: number
|
|
34
56
|
configAction: string | null
|
|
35
57
|
}
|
|
36
58
|
|
|
37
59
|
/**
|
|
38
|
-
* Check if
|
|
60
|
+
* Check if an AI CLI is installed
|
|
39
61
|
*/
|
|
40
|
-
async function
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
return true
|
|
44
|
-
} catch (_error) {
|
|
45
|
-
return false
|
|
46
|
-
}
|
|
62
|
+
async function hasAICLI(provider: AIProviderConfig): Promise<boolean> {
|
|
63
|
+
const detection = detectProvider(provider.name)
|
|
64
|
+
return detection.installed
|
|
47
65
|
}
|
|
48
66
|
|
|
49
67
|
/**
|
|
50
|
-
* Install
|
|
68
|
+
* Install AI CLI for the specified provider
|
|
51
69
|
*/
|
|
52
|
-
async function
|
|
70
|
+
async function installAICLI(provider: AIProviderConfig): Promise<boolean> {
|
|
71
|
+
const packageName = provider.name === 'claude'
|
|
72
|
+
? '@anthropic-ai/claude-code'
|
|
73
|
+
: '@google/gemini-cli'
|
|
74
|
+
|
|
53
75
|
try {
|
|
54
|
-
console.log(`${YELLOW}📦
|
|
76
|
+
console.log(`${YELLOW}📦 ${provider.displayName} not found. Installing...${NC}`)
|
|
55
77
|
console.log('')
|
|
56
|
-
execSync(
|
|
78
|
+
execSync(`npm install -g ${packageName}`, { stdio: 'inherit' })
|
|
57
79
|
console.log('')
|
|
58
|
-
console.log(`${GREEN}✓${NC}
|
|
80
|
+
console.log(`${GREEN}✓${NC} ${provider.displayName} installed successfully`)
|
|
59
81
|
console.log('')
|
|
60
82
|
return true
|
|
61
83
|
} catch (error) {
|
|
62
|
-
console.log(`${YELLOW}⚠️ Failed to install
|
|
63
|
-
console.log(`${DIM}Please install manually: npm install -g
|
|
84
|
+
console.log(`${YELLOW}⚠️ Failed to install ${provider.displayName}: ${(error as Error).message}${NC}`)
|
|
85
|
+
console.log(`${DIM}Please install manually: npm install -g ${packageName}${NC}`)
|
|
64
86
|
console.log('')
|
|
65
87
|
return false
|
|
66
88
|
}
|
|
67
89
|
}
|
|
68
90
|
|
|
69
91
|
/**
|
|
70
|
-
* Main setup function
|
|
92
|
+
* Main setup function - installs for ALL detected providers
|
|
71
93
|
*/
|
|
72
94
|
export async function run(): Promise<SetupResults> {
|
|
95
|
+
// Step 0: Detect all available providers
|
|
96
|
+
const detection = detectAllProviders()
|
|
97
|
+
const selection = selectProvider()
|
|
98
|
+
const primaryProvider = Providers[selection.provider]
|
|
99
|
+
|
|
73
100
|
const results: SetupResults = {
|
|
74
|
-
|
|
101
|
+
provider: selection.provider,
|
|
102
|
+
providers: [],
|
|
103
|
+
cliInstalled: false,
|
|
75
104
|
commandsAdded: 0,
|
|
76
105
|
commandsUpdated: 0,
|
|
77
106
|
configAction: null,
|
|
78
107
|
}
|
|
79
108
|
|
|
80
|
-
// Step 1:
|
|
81
|
-
const
|
|
109
|
+
// Step 1: Install for each detected provider
|
|
110
|
+
const providerNames: AIProviderName[] = ['claude', 'gemini']
|
|
82
111
|
|
|
83
|
-
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
112
|
+
for (const providerName of providerNames) {
|
|
113
|
+
const providerConfig = Providers[providerName]
|
|
114
|
+
const providerDetection = detection[providerName]
|
|
115
|
+
|
|
116
|
+
const providerResult: ProviderSetupResult = {
|
|
117
|
+
provider: providerName,
|
|
118
|
+
cliInstalled: false,
|
|
119
|
+
commandsAdded: 0,
|
|
120
|
+
commandsUpdated: 0,
|
|
121
|
+
configAction: null,
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Check if CLI is installed
|
|
125
|
+
if (!providerDetection.installed) {
|
|
126
|
+
// Only prompt to install the primary (selected) provider
|
|
127
|
+
if (providerName === selection.provider) {
|
|
128
|
+
const installed = await installAICLI(providerConfig)
|
|
129
|
+
if (installed) {
|
|
130
|
+
providerResult.cliInstalled = true
|
|
131
|
+
results.cliInstalled = true
|
|
132
|
+
} else {
|
|
133
|
+
throw new Error(`${providerConfig.displayName} installation failed`)
|
|
134
|
+
}
|
|
135
|
+
} else {
|
|
136
|
+
// Skip non-primary providers that aren't installed
|
|
137
|
+
continue
|
|
138
|
+
}
|
|
89
139
|
}
|
|
90
|
-
}
|
|
91
140
|
|
|
92
|
-
|
|
93
|
-
|
|
141
|
+
// Step 2: Install commands and config for this provider
|
|
142
|
+
if (providerName === 'claude') {
|
|
143
|
+
const claudeDetected = await installer.detectClaude()
|
|
144
|
+
|
|
145
|
+
if (claudeDetected) {
|
|
146
|
+
// Sync commands
|
|
147
|
+
const syncResult = await installer.syncCommands()
|
|
148
|
+
if (syncResult.success) {
|
|
149
|
+
providerResult.commandsAdded = syncResult.added
|
|
150
|
+
providerResult.commandsUpdated = syncResult.updated
|
|
151
|
+
results.commandsAdded += syncResult.added
|
|
152
|
+
results.commandsUpdated += syncResult.updated
|
|
153
|
+
}
|
|
94
154
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
155
|
+
// Install global configuration
|
|
156
|
+
const configResult = await installer.installGlobalConfig()
|
|
157
|
+
if (configResult.success) {
|
|
158
|
+
providerResult.configAction = configResult.action
|
|
159
|
+
if (!results.configAction) {
|
|
160
|
+
results.configAction = configResult.action
|
|
161
|
+
}
|
|
162
|
+
}
|
|
98
163
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
results.commandsUpdated = syncResult.updated
|
|
102
|
-
}
|
|
164
|
+
// Install documentation files
|
|
165
|
+
await installer.installDocs()
|
|
103
166
|
|
|
104
|
-
|
|
105
|
-
|
|
167
|
+
// Install status line (Claude only)
|
|
168
|
+
await installStatusLine()
|
|
169
|
+
}
|
|
170
|
+
} else if (providerName === 'gemini') {
|
|
171
|
+
// Gemini provider - install router and global config
|
|
172
|
+
const geminiInstalled = await installGeminiRouter()
|
|
173
|
+
if (geminiInstalled) {
|
|
174
|
+
providerResult.commandsAdded = 1
|
|
175
|
+
results.commandsAdded += 1
|
|
176
|
+
}
|
|
106
177
|
|
|
107
|
-
|
|
108
|
-
|
|
178
|
+
const geminiConfigResult = await installGeminiGlobalConfig()
|
|
179
|
+
if (geminiConfigResult.success) {
|
|
180
|
+
providerResult.configAction = geminiConfigResult.action
|
|
181
|
+
}
|
|
109
182
|
}
|
|
110
183
|
|
|
111
|
-
|
|
112
|
-
await installer.installDocs()
|
|
113
|
-
|
|
114
|
-
// Step 4c: Install status line with version check
|
|
115
|
-
await installStatusLine()
|
|
184
|
+
results.providers.push(providerResult)
|
|
116
185
|
}
|
|
117
186
|
|
|
118
|
-
// Step
|
|
119
|
-
await editorsConfig.saveConfig(VERSION, installer.getInstallPath())
|
|
187
|
+
// Step 3: Save version in editors-config
|
|
188
|
+
await editorsConfig.saveConfig(VERSION, installer.getInstallPath(), selection.provider)
|
|
120
189
|
|
|
121
|
-
// Step
|
|
190
|
+
// Step 4: Migrate existing projects to add cliVersion
|
|
122
191
|
await migrateProjectsCliVersion()
|
|
123
192
|
|
|
124
|
-
// Show results
|
|
125
|
-
|
|
193
|
+
// Show results for all providers
|
|
194
|
+
for (const providerResult of results.providers) {
|
|
195
|
+
showResults(providerResult, Providers[providerResult.provider])
|
|
196
|
+
}
|
|
126
197
|
|
|
127
198
|
return results
|
|
128
199
|
}
|
|
@@ -130,6 +201,100 @@ export async function run(): Promise<SetupResults> {
|
|
|
130
201
|
// Default export for CommonJS require
|
|
131
202
|
export default { run }
|
|
132
203
|
|
|
204
|
+
/**
|
|
205
|
+
* Install the p.toml router for Gemini CLI
|
|
206
|
+
*/
|
|
207
|
+
async function installGeminiRouter(): Promise<boolean> {
|
|
208
|
+
try {
|
|
209
|
+
const geminiCommandsDir = path.join(os.homedir(), '.gemini', 'commands')
|
|
210
|
+
const routerSource = path.join(getPackageRoot(), 'templates', 'commands', 'p.toml')
|
|
211
|
+
const routerDest = path.join(geminiCommandsDir, 'p.toml')
|
|
212
|
+
|
|
213
|
+
// Ensure commands directory exists
|
|
214
|
+
fs.mkdirSync(geminiCommandsDir, { recursive: true })
|
|
215
|
+
|
|
216
|
+
// Copy router
|
|
217
|
+
if (fs.existsSync(routerSource)) {
|
|
218
|
+
fs.copyFileSync(routerSource, routerDest)
|
|
219
|
+
return true
|
|
220
|
+
}
|
|
221
|
+
return false
|
|
222
|
+
} catch (error) {
|
|
223
|
+
console.error(`Gemini router warning: ${(error as Error).message}`)
|
|
224
|
+
return false
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Install or update global GEMINI.md configuration
|
|
230
|
+
*/
|
|
231
|
+
async function installGeminiGlobalConfig(): Promise<{ success: boolean; action: string | null }> {
|
|
232
|
+
try {
|
|
233
|
+
const geminiDir = path.join(os.homedir(), '.gemini')
|
|
234
|
+
const globalConfigPath = path.join(geminiDir, 'GEMINI.md')
|
|
235
|
+
const templatePath = path.join(getPackageRoot(), 'templates', 'global', 'GEMINI.md')
|
|
236
|
+
|
|
237
|
+
// Ensure ~/.gemini directory exists
|
|
238
|
+
fs.mkdirSync(geminiDir, { recursive: true })
|
|
239
|
+
|
|
240
|
+
// Read template content
|
|
241
|
+
const templateContent = fs.readFileSync(templatePath, 'utf-8')
|
|
242
|
+
|
|
243
|
+
// Check if global config already exists
|
|
244
|
+
let existingContent = ''
|
|
245
|
+
let fileExists = false
|
|
246
|
+
|
|
247
|
+
try {
|
|
248
|
+
existingContent = fs.readFileSync(globalConfigPath, 'utf-8')
|
|
249
|
+
fileExists = true
|
|
250
|
+
} catch (error) {
|
|
251
|
+
if (isNotFoundError(error)) {
|
|
252
|
+
fileExists = false
|
|
253
|
+
} else {
|
|
254
|
+
throw error
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (!fileExists) {
|
|
259
|
+
// Create new file with full template
|
|
260
|
+
fs.writeFileSync(globalConfigPath, templateContent, 'utf-8')
|
|
261
|
+
return { success: true, action: 'created' }
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// File exists - perform intelligent merge
|
|
265
|
+
const startMarker = '<!-- prjct:start - DO NOT REMOVE THIS MARKER -->'
|
|
266
|
+
const endMarker = '<!-- prjct:end - DO NOT REMOVE THIS MARKER -->'
|
|
267
|
+
|
|
268
|
+
const hasMarkers = existingContent.includes(startMarker) && existingContent.includes(endMarker)
|
|
269
|
+
|
|
270
|
+
if (!hasMarkers) {
|
|
271
|
+
// No markers - append prjct section at the end
|
|
272
|
+
const updatedContent = existingContent + '\n\n' + templateContent
|
|
273
|
+
fs.writeFileSync(globalConfigPath, updatedContent, 'utf-8')
|
|
274
|
+
return { success: true, action: 'appended' }
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Markers exist - replace content between markers
|
|
278
|
+
const beforeMarker = existingContent.substring(0, existingContent.indexOf(startMarker))
|
|
279
|
+
const afterMarker = existingContent.substring(
|
|
280
|
+
existingContent.indexOf(endMarker) + endMarker.length
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
// Extract prjct section from template
|
|
284
|
+
const prjctSection = templateContent.substring(
|
|
285
|
+
templateContent.indexOf(startMarker),
|
|
286
|
+
templateContent.indexOf(endMarker) + endMarker.length
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
const updatedContent = beforeMarker + prjctSection + afterMarker
|
|
290
|
+
fs.writeFileSync(globalConfigPath, updatedContent, 'utf-8')
|
|
291
|
+
return { success: true, action: 'updated' }
|
|
292
|
+
} catch (error) {
|
|
293
|
+
console.error(`Gemini config warning: ${(error as Error).message}`)
|
|
294
|
+
return { success: false, action: null }
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
133
298
|
/**
|
|
134
299
|
* Migrate existing projects to add cliVersion field
|
|
135
300
|
* This clears the status line warning after npm update
|
|
@@ -415,15 +580,15 @@ function ensureStatusLineSymlink(linkPath: string, targetPath: string): void {
|
|
|
415
580
|
}
|
|
416
581
|
|
|
417
582
|
/**
|
|
418
|
-
* Show setup results
|
|
583
|
+
* Show setup results for a single provider
|
|
419
584
|
*/
|
|
420
|
-
function showResults(results:
|
|
585
|
+
function showResults(results: ProviderSetupResult, provider: AIProviderConfig): void {
|
|
421
586
|
console.log('')
|
|
422
587
|
|
|
423
|
-
if (results.
|
|
424
|
-
console.log(` ${GREEN}✓${NC}
|
|
588
|
+
if (results.cliInstalled) {
|
|
589
|
+
console.log(` ${GREEN}✓${NC} ${provider.displayName} CLI installed`)
|
|
425
590
|
} else {
|
|
426
|
-
console.log(` ${GREEN}✓${NC}
|
|
591
|
+
console.log(` ${GREEN}✓${NC} ${provider.displayName} CLI found`)
|
|
427
592
|
}
|
|
428
593
|
|
|
429
594
|
const totalCommands = results.commandsAdded + results.commandsUpdated
|
|
@@ -437,11 +602,11 @@ function showResults(results: SetupResults): void {
|
|
|
437
602
|
}
|
|
438
603
|
|
|
439
604
|
if (results.configAction === 'created') {
|
|
440
|
-
console.log(` ${GREEN}✓${NC} Global config created`)
|
|
605
|
+
console.log(` ${GREEN}✓${NC} Global config created (${provider.contextFile})`)
|
|
441
606
|
} else if (results.configAction === 'updated') {
|
|
442
|
-
console.log(` ${GREEN}✓${NC} Global config updated`)
|
|
607
|
+
console.log(` ${GREEN}✓${NC} Global config updated (${provider.contextFile})`)
|
|
443
608
|
} else if (results.configAction === 'appended') {
|
|
444
|
-
console.log(` ${GREEN}✓${NC} Global config merged`)
|
|
609
|
+
console.log(` ${GREEN}✓${NC} Global config merged (${provider.contextFile})`)
|
|
445
610
|
}
|
|
446
611
|
|
|
447
612
|
console.log('')
|
|
@@ -6,10 +6,14 @@
|
|
|
6
6
|
*
|
|
7
7
|
* Skill sources (in priority order):
|
|
8
8
|
* 1. Project: .prjct/skills/*.md
|
|
9
|
-
* 2.
|
|
10
|
-
* 3.
|
|
9
|
+
* 2. Provider: ~/.claude/skills/* or ~/.gemini/skills/* (SKILL.md format)
|
|
10
|
+
* 3. Global: ~/.prjct-cli/skills/*.md
|
|
11
|
+
* 4. Built-in: templates/skills/*.md
|
|
11
12
|
*
|
|
12
|
-
*
|
|
13
|
+
* Note: Claude Code and Gemini CLI use identical SKILL.md format,
|
|
14
|
+
* so skills are compatible between both providers.
|
|
15
|
+
*
|
|
16
|
+
* @version 1.1.0
|
|
13
17
|
*/
|
|
14
18
|
|
|
15
19
|
import fs from 'fs/promises'
|
|
@@ -17,6 +21,7 @@ import path from 'path'
|
|
|
17
21
|
import { glob } from 'glob'
|
|
18
22
|
|
|
19
23
|
import type { SkillMetadata, Skill, SkillSearchResult } from '../types'
|
|
24
|
+
import type { AIProviderName } from '../types/provider'
|
|
20
25
|
|
|
21
26
|
/**
|
|
22
27
|
* Parse YAML-like frontmatter from markdown
|
|
@@ -57,9 +62,17 @@ function parseFrontmatter(content: string): { metadata: Record<string, unknown>;
|
|
|
57
62
|
|
|
58
63
|
/**
|
|
59
64
|
* Convert filename to skill ID
|
|
65
|
+
* For SKILL.md files, uses the parent directory name
|
|
60
66
|
*/
|
|
61
67
|
function fileToSkillId(filePath: string): string {
|
|
62
68
|
const basename = path.basename(filePath, '.md')
|
|
69
|
+
|
|
70
|
+
// For SKILL.md files (provider format), use parent directory name
|
|
71
|
+
if (basename.toUpperCase() === 'SKILL') {
|
|
72
|
+
const dirName = path.basename(path.dirname(filePath))
|
|
73
|
+
return dirName.toLowerCase().replace(/[^a-z0-9]+/g, '-')
|
|
74
|
+
}
|
|
75
|
+
|
|
63
76
|
return basename.toLowerCase().replace(/[^a-z0-9]+/g, '-')
|
|
64
77
|
}
|
|
65
78
|
|
|
@@ -70,16 +83,27 @@ class SkillService {
|
|
|
70
83
|
/**
|
|
71
84
|
* Get all skill directories in order of priority
|
|
72
85
|
*/
|
|
73
|
-
private getSkillDirs(projectPath?: string): Array<{ dir: string; source: Skill['source'] }> {
|
|
86
|
+
private getSkillDirs(projectPath?: string, provider?: AIProviderName): Array<{ dir: string; source: Skill['source']; isProviderSkill?: boolean }> {
|
|
74
87
|
const homeDir = process.env.HOME || process.env.USERPROFILE || '~'
|
|
75
|
-
const dirs: Array<{ dir: string; source: Skill['source'] }> = []
|
|
88
|
+
const dirs: Array<{ dir: string; source: Skill['source']; isProviderSkill?: boolean }> = []
|
|
76
89
|
|
|
77
90
|
// Project skills (highest priority)
|
|
78
91
|
if (projectPath) {
|
|
79
92
|
dirs.push({ dir: path.join(projectPath, '.prjct', 'skills'), source: 'project' })
|
|
80
93
|
}
|
|
81
94
|
|
|
82
|
-
//
|
|
95
|
+
// Provider skills (Claude or Gemini)
|
|
96
|
+
// Both use SKILL.md format, so skills are compatible
|
|
97
|
+
if (provider) {
|
|
98
|
+
const providerDir = provider === 'gemini' ? '.gemini' : '.claude'
|
|
99
|
+
dirs.push({ dir: path.join(homeDir, providerDir, 'skills'), source: 'global', isProviderSkill: true })
|
|
100
|
+
} else {
|
|
101
|
+
// Check both providers if no specific one is set
|
|
102
|
+
dirs.push({ dir: path.join(homeDir, '.claude', 'skills'), source: 'global', isProviderSkill: true })
|
|
103
|
+
dirs.push({ dir: path.join(homeDir, '.gemini', 'skills'), source: 'global', isProviderSkill: true })
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// prjct global skills
|
|
83
107
|
dirs.push({ dir: path.join(homeDir, '.prjct-cli', 'skills'), source: 'global' })
|
|
84
108
|
|
|
85
109
|
// Built-in skills (lowest priority)
|
|
@@ -123,19 +147,31 @@ class SkillService {
|
|
|
123
147
|
/**
|
|
124
148
|
* Load all skills from all sources
|
|
125
149
|
*/
|
|
126
|
-
async loadSkills(projectPath?: string): Promise<void> {
|
|
150
|
+
async loadSkills(projectPath?: string, provider?: AIProviderName): Promise<void> {
|
|
127
151
|
this.skills.clear()
|
|
128
|
-
const dirs = this.getSkillDirs(projectPath)
|
|
152
|
+
const dirs = this.getSkillDirs(projectPath, provider)
|
|
129
153
|
|
|
130
|
-
for (const { dir, source } of dirs) {
|
|
154
|
+
for (const { dir, source, isProviderSkill } of dirs) {
|
|
131
155
|
try {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
this.skills.
|
|
156
|
+
if (isProviderSkill) {
|
|
157
|
+
// Provider skills use SKILL.md in subdirectories
|
|
158
|
+
// e.g., ~/.claude/skills/my-skill/SKILL.md
|
|
159
|
+
const skillDirs = await glob('*/SKILL.md', { cwd: dir, absolute: true })
|
|
160
|
+
for (const file of skillDirs) {
|
|
161
|
+
const skill = await this.loadSkill(file, source)
|
|
162
|
+
if (skill && !this.skills.has(skill.id)) {
|
|
163
|
+
this.skills.set(skill.id, skill)
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
} else {
|
|
167
|
+
// Regular .md files in directory
|
|
168
|
+
const files = await glob('*.md', { cwd: dir, absolute: true })
|
|
169
|
+
for (const file of files) {
|
|
170
|
+
const skill = await this.loadSkill(file, source)
|
|
171
|
+
if (skill && !this.skills.has(skill.id)) {
|
|
172
|
+
// Don't override higher priority skills
|
|
173
|
+
this.skills.set(skill.id, skill)
|
|
174
|
+
}
|
|
139
175
|
}
|
|
140
176
|
}
|
|
141
177
|
} catch (_error) {
|
package/core/types/index.ts
CHANGED
|
@@ -405,3 +405,15 @@ export type {
|
|
|
405
405
|
SSEManager,
|
|
406
406
|
SSEEventType,
|
|
407
407
|
} from './server'
|
|
408
|
+
|
|
409
|
+
// =============================================================================
|
|
410
|
+
// Provider Types (AI CLI abstraction)
|
|
411
|
+
// =============================================================================
|
|
412
|
+
export type {
|
|
413
|
+
AIProviderName,
|
|
414
|
+
CommandFormat,
|
|
415
|
+
AIProviderConfig,
|
|
416
|
+
ProviderDetectionResult,
|
|
417
|
+
ProviderSelectionResult,
|
|
418
|
+
ProviderBranding,
|
|
419
|
+
} from './provider'
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Provider Types
|
|
3
|
+
*
|
|
4
|
+
* Abstractions for supporting multiple AI CLI agents (Claude Code, Gemini CLI).
|
|
5
|
+
* Both agents share similar architectures, making compatibility achievable.
|
|
6
|
+
*
|
|
7
|
+
* Key discovery: Skills use identical SKILL.md format for both providers.
|
|
8
|
+
*
|
|
9
|
+
* @see https://geminicli.com/docs/cli/gemini-md/
|
|
10
|
+
* @see https://geminicli.com/docs/cli/skills/
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Supported AI provider names
|
|
15
|
+
*/
|
|
16
|
+
export type AIProviderName = 'claude' | 'gemini'
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Command format for each provider
|
|
20
|
+
* - Claude: Markdown files (.md)
|
|
21
|
+
* - Gemini: TOML files (.toml)
|
|
22
|
+
*/
|
|
23
|
+
export type CommandFormat = 'md' | 'toml'
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* AI Provider configuration
|
|
27
|
+
* Defines paths and formats for each AI CLI agent
|
|
28
|
+
*/
|
|
29
|
+
export interface AIProviderConfig {
|
|
30
|
+
/** Provider identifier */
|
|
31
|
+
name: AIProviderName
|
|
32
|
+
|
|
33
|
+
/** Display name for UI/logs */
|
|
34
|
+
displayName: string
|
|
35
|
+
|
|
36
|
+
/** CLI command name (e.g., 'claude', 'gemini') */
|
|
37
|
+
cliCommand: string
|
|
38
|
+
|
|
39
|
+
/** Global config directory (e.g., ~/.claude, ~/.gemini) */
|
|
40
|
+
configDir: string
|
|
41
|
+
|
|
42
|
+
/** Context file name (CLAUDE.md or GEMINI.md) */
|
|
43
|
+
contextFile: string
|
|
44
|
+
|
|
45
|
+
/** Skills directory (e.g., ~/.claude/skills, ~/.gemini/skills) */
|
|
46
|
+
skillsDir: string
|
|
47
|
+
|
|
48
|
+
/** Commands directory relative to project (e.g., .claude/commands, .gemini/commands) */
|
|
49
|
+
commandsDir: string
|
|
50
|
+
|
|
51
|
+
/** Command file format */
|
|
52
|
+
commandFormat: CommandFormat
|
|
53
|
+
|
|
54
|
+
/** Settings file name (settings.json for both) */
|
|
55
|
+
settingsFile: string
|
|
56
|
+
|
|
57
|
+
/** Project settings file (e.g., settings.local.json, settings.json) */
|
|
58
|
+
projectSettingsFile: string
|
|
59
|
+
|
|
60
|
+
/** Ignore file name (.claudeignore, .geminiignore) */
|
|
61
|
+
ignoreFile: string
|
|
62
|
+
|
|
63
|
+
/** URL for provider website */
|
|
64
|
+
websiteUrl: string
|
|
65
|
+
|
|
66
|
+
/** URL for provider documentation */
|
|
67
|
+
docsUrl: string
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Provider detection result
|
|
72
|
+
*/
|
|
73
|
+
export interface ProviderDetectionResult {
|
|
74
|
+
/** Whether the provider CLI is installed */
|
|
75
|
+
installed: boolean
|
|
76
|
+
|
|
77
|
+
/** Provider version if installed */
|
|
78
|
+
version?: string
|
|
79
|
+
|
|
80
|
+
/** Path to the CLI executable */
|
|
81
|
+
path?: string
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Result of provider selection during setup
|
|
86
|
+
*/
|
|
87
|
+
export interface ProviderSelectionResult {
|
|
88
|
+
/** Selected provider */
|
|
89
|
+
provider: AIProviderName
|
|
90
|
+
|
|
91
|
+
/** Whether user was prompted to choose (both installed) */
|
|
92
|
+
userSelected: boolean
|
|
93
|
+
|
|
94
|
+
/** Detection details */
|
|
95
|
+
detection: {
|
|
96
|
+
claude: ProviderDetectionResult
|
|
97
|
+
gemini: ProviderDetectionResult
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Provider-aware branding configuration
|
|
103
|
+
*/
|
|
104
|
+
export interface ProviderBranding {
|
|
105
|
+
/** Commit footer text */
|
|
106
|
+
commitFooter: string
|
|
107
|
+
|
|
108
|
+
/** Short signature */
|
|
109
|
+
signature: string
|
|
110
|
+
}
|
package/core/utils/branding.ts
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Branding Configuration for prjct-cli
|
|
3
|
-
* Single source of truth for all branding across CLI and
|
|
3
|
+
* Single source of truth for all branding across CLI and AI agents
|
|
4
|
+
*
|
|
5
|
+
* Supports multiple AI providers (Claude Code, Gemini CLI)
|
|
4
6
|
*/
|
|
5
7
|
|
|
6
8
|
import chalk from 'chalk'
|
|
9
|
+
import type { AIProviderName } from '../types/provider'
|
|
10
|
+
import { getProviderBranding, Providers } from '../infrastructure/ai-provider'
|
|
7
11
|
|
|
8
12
|
const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
|
|
9
13
|
const SPINNER_SPEED = 80
|
|
@@ -30,6 +34,9 @@ interface Branding {
|
|
|
30
34
|
website: string
|
|
31
35
|
docs: string
|
|
32
36
|
}
|
|
37
|
+
// Provider-aware methods
|
|
38
|
+
getCommitFooter: (provider?: AIProviderName) => string
|
|
39
|
+
getSignature: (provider?: AIProviderName) => string
|
|
33
40
|
}
|
|
34
41
|
|
|
35
42
|
const branding: Branding = {
|
|
@@ -52,13 +59,13 @@ const branding: Branding = {
|
|
|
52
59
|
chalk.cyan('⚡') + ' ' + chalk.cyan('prjct') + ' ' + chalk.cyan(SPINNER_FRAMES[frame % 10]) + ' ' + chalk.dim(msg || '')
|
|
53
60
|
},
|
|
54
61
|
|
|
55
|
-
// Template
|
|
62
|
+
// Template (plain text)
|
|
56
63
|
template: {
|
|
57
64
|
header: '⚡ prjct',
|
|
58
65
|
footer: '⚡ prjct'
|
|
59
66
|
},
|
|
60
67
|
|
|
61
|
-
// Git commit footer
|
|
68
|
+
// Default Git commit footer (Claude - for backward compatibility)
|
|
62
69
|
commitFooter: `🤖 Generated with [p/](https://www.prjct.app/)
|
|
63
70
|
Designed for [Claude](https://www.anthropic.com/claude)`,
|
|
64
71
|
|
|
@@ -66,6 +73,16 @@ Designed for [Claude](https://www.anthropic.com/claude)`,
|
|
|
66
73
|
urls: {
|
|
67
74
|
website: 'https://prjct.app',
|
|
68
75
|
docs: 'https://prjct.app/docs'
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
// Provider-aware commit footer
|
|
79
|
+
getCommitFooter: (provider: AIProviderName = 'claude') => {
|
|
80
|
+
return getProviderBranding(provider).commitFooter
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
// Provider-aware signature
|
|
84
|
+
getSignature: (provider: AIProviderName = 'claude') => {
|
|
85
|
+
return getProviderBranding(provider).signature
|
|
69
86
|
}
|
|
70
87
|
}
|
|
71
88
|
|