prjct-cli 0.37.1 → 0.40.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 +123 -0
- package/README.md +44 -23
- package/bin/prjct.ts +11 -1
- package/core/agentic/orchestrator-executor.ts +23 -6
- package/core/index.ts +14 -6
- package/core/infrastructure/ai-provider.ts +157 -1
- package/core/infrastructure/setup.ts +204 -0
- package/core/services/skill-installer.ts +431 -0
- package/core/services/skill-lock.ts +132 -0
- package/core/services/skill-service.ts +34 -24
- package/core/types/provider.ts +18 -1
- package/core/types/services.ts +6 -1
- package/dist/bin/prjct.mjs +3607 -1050
- package/dist/core/infrastructure/command-installer.js +458 -47
- package/dist/core/infrastructure/setup.js +728 -211
- package/package.json +1 -1
- package/templates/antigravity/SKILL.md +39 -0
- package/templates/commands/skill.md +174 -25
- package/templates/global/ANTIGRAVITY.md +256 -0
- package/templates/global/CLAUDE.md +30 -0
- package/templates/global/CURSOR.mdc +30 -0
- package/templates/global/GEMINI.md +30 -0
- package/templates/global/WINDSURF.md +268 -0
- package/templates/windsurf/router.md +28 -0
- package/templates/windsurf/workflows/bug.md +8 -0
- package/templates/windsurf/workflows/done.md +4 -0
- package/templates/windsurf/workflows/pause.md +4 -0
- package/templates/windsurf/workflows/resume.md +4 -0
- package/templates/windsurf/workflows/ship.md +8 -0
- package/templates/windsurf/workflows/sync.md +4 -0
- package/templates/windsurf/workflows/task.md +8 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill Lock File Service
|
|
3
|
+
*
|
|
4
|
+
* Manages a lock file that tracks remotely-installed skills for
|
|
5
|
+
* reproducibility and update detection.
|
|
6
|
+
*
|
|
7
|
+
* Lock file location: ~/.prjct-cli/skills/.skill-lock.json
|
|
8
|
+
*
|
|
9
|
+
* @version 1.0.0
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import fs from 'fs/promises'
|
|
13
|
+
import path from 'path'
|
|
14
|
+
import os from 'os'
|
|
15
|
+
|
|
16
|
+
// =============================================================================
|
|
17
|
+
// Types
|
|
18
|
+
// =============================================================================
|
|
19
|
+
|
|
20
|
+
export interface SkillLockSource {
|
|
21
|
+
type: 'github' | 'local' | 'registry'
|
|
22
|
+
url: string
|
|
23
|
+
sha?: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface SkillLockEntry {
|
|
27
|
+
name: string
|
|
28
|
+
source: SkillLockSource
|
|
29
|
+
installedAt: string
|
|
30
|
+
filePath: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface SkillLockFile {
|
|
34
|
+
version: 1
|
|
35
|
+
generatedAt: string
|
|
36
|
+
skills: Record<string, SkillLockEntry>
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// =============================================================================
|
|
40
|
+
// Lock File Service
|
|
41
|
+
// =============================================================================
|
|
42
|
+
|
|
43
|
+
const LOCK_FILE_NAME = '.skill-lock.json'
|
|
44
|
+
|
|
45
|
+
function getLockFilePath(): string {
|
|
46
|
+
return path.join(os.homedir(), '.prjct-cli', 'skills', LOCK_FILE_NAME)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function createEmptyLockFile(): SkillLockFile {
|
|
50
|
+
return {
|
|
51
|
+
version: 1,
|
|
52
|
+
generatedAt: new Date().toISOString(),
|
|
53
|
+
skills: {},
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Read the lock file, returning an empty lock file if it doesn't exist
|
|
59
|
+
*/
|
|
60
|
+
async function read(): Promise<SkillLockFile> {
|
|
61
|
+
try {
|
|
62
|
+
const content = await fs.readFile(getLockFilePath(), 'utf-8')
|
|
63
|
+
return JSON.parse(content) as SkillLockFile
|
|
64
|
+
} catch {
|
|
65
|
+
return createEmptyLockFile()
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Write the lock file to disk
|
|
71
|
+
*/
|
|
72
|
+
async function write(lockFile: SkillLockFile): Promise<void> {
|
|
73
|
+
const lockPath = getLockFilePath()
|
|
74
|
+
await fs.mkdir(path.dirname(lockPath), { recursive: true })
|
|
75
|
+
lockFile.generatedAt = new Date().toISOString()
|
|
76
|
+
await fs.writeFile(lockPath, JSON.stringify(lockFile, null, 2), 'utf-8')
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Add or update a skill entry in the lock file
|
|
81
|
+
*/
|
|
82
|
+
async function addEntry(entry: SkillLockEntry): Promise<void> {
|
|
83
|
+
const lockFile = await read()
|
|
84
|
+
lockFile.skills[entry.name] = entry
|
|
85
|
+
await write(lockFile)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Remove a skill entry from the lock file
|
|
90
|
+
*/
|
|
91
|
+
async function removeEntry(name: string): Promise<boolean> {
|
|
92
|
+
const lockFile = await read()
|
|
93
|
+
if (!(name in lockFile.skills)) return false
|
|
94
|
+
delete lockFile.skills[name]
|
|
95
|
+
await write(lockFile)
|
|
96
|
+
return true
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Get a single skill entry
|
|
101
|
+
*/
|
|
102
|
+
async function getEntry(name: string): Promise<SkillLockEntry | null> {
|
|
103
|
+
const lockFile = await read()
|
|
104
|
+
return lockFile.skills[name] || null
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Get all entries
|
|
109
|
+
*/
|
|
110
|
+
async function getAll(): Promise<Record<string, SkillLockEntry>> {
|
|
111
|
+
const lockFile = await read()
|
|
112
|
+
return lockFile.skills
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Get the lock file path (for display purposes)
|
|
117
|
+
*/
|
|
118
|
+
function getPath(): string {
|
|
119
|
+
return getLockFilePath()
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export const skillLock = {
|
|
123
|
+
read,
|
|
124
|
+
write,
|
|
125
|
+
addEntry,
|
|
126
|
+
removeEntry,
|
|
127
|
+
getEntry,
|
|
128
|
+
getAll,
|
|
129
|
+
getPath,
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export default skillLock
|
|
@@ -83,9 +83,9 @@ class SkillService {
|
|
|
83
83
|
/**
|
|
84
84
|
* Get all skill directories in order of priority
|
|
85
85
|
*/
|
|
86
|
-
private getSkillDirs(projectPath?: string, provider?: AIProviderName): Array<{ dir: string; source: Skill['source']
|
|
86
|
+
private getSkillDirs(projectPath?: string, provider?: AIProviderName): Array<{ dir: string; source: Skill['source'] }> {
|
|
87
87
|
const homeDir = process.env.HOME || process.env.USERPROFILE || '~'
|
|
88
|
-
const dirs: Array<{ dir: string; source: Skill['source']
|
|
88
|
+
const dirs: Array<{ dir: string; source: Skill['source'] }> = []
|
|
89
89
|
|
|
90
90
|
// Project skills (highest priority)
|
|
91
91
|
if (projectPath) {
|
|
@@ -96,11 +96,11 @@ class SkillService {
|
|
|
96
96
|
// Both use SKILL.md format, so skills are compatible
|
|
97
97
|
if (provider) {
|
|
98
98
|
const providerDir = provider === 'gemini' ? '.gemini' : '.claude'
|
|
99
|
-
dirs.push({ dir: path.join(homeDir, providerDir, 'skills'), source: 'global'
|
|
99
|
+
dirs.push({ dir: path.join(homeDir, providerDir, 'skills'), source: 'global' })
|
|
100
100
|
} else {
|
|
101
101
|
// Check both providers if no specific one is set
|
|
102
|
-
dirs.push({ dir: path.join(homeDir, '.claude', 'skills'), source: 'global'
|
|
103
|
-
dirs.push({ dir: path.join(homeDir, '.gemini', 'skills'), source: 'global'
|
|
102
|
+
dirs.push({ dir: path.join(homeDir, '.claude', 'skills'), source: 'global' })
|
|
103
|
+
dirs.push({ dir: path.join(homeDir, '.gemini', 'skills'), source: 'global' })
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
// prjct global skills
|
|
@@ -123,6 +123,9 @@ class SkillService {
|
|
|
123
123
|
const id = fileToSkillId(filePath)
|
|
124
124
|
const name = (metadata.name as string) || id
|
|
125
125
|
|
|
126
|
+
// Extract _prjct source tracking metadata if present
|
|
127
|
+
const prjctMeta = metadata._prjct as Record<string, unknown> | undefined
|
|
128
|
+
|
|
126
129
|
return {
|
|
127
130
|
id,
|
|
128
131
|
name,
|
|
@@ -136,6 +139,12 @@ class SkillService {
|
|
|
136
139
|
agent: metadata.agent as string,
|
|
137
140
|
tags: metadata.tags as string[],
|
|
138
141
|
version: metadata.version as string,
|
|
142
|
+
category: metadata.category as string,
|
|
143
|
+
author: metadata.author as string,
|
|
144
|
+
sourceUrl: prjctMeta?.sourceUrl as string,
|
|
145
|
+
sourceType: prjctMeta?.sourceType as SkillMetadata['sourceType'],
|
|
146
|
+
installedAt: prjctMeta?.installedAt as string,
|
|
147
|
+
sha: prjctMeta?.sha as string,
|
|
139
148
|
},
|
|
140
149
|
}
|
|
141
150
|
} catch (_error) {
|
|
@@ -151,27 +160,27 @@ class SkillService {
|
|
|
151
160
|
this.skills.clear()
|
|
152
161
|
const dirs = this.getSkillDirs(projectPath, provider)
|
|
153
162
|
|
|
154
|
-
for (const { dir, source
|
|
163
|
+
for (const { dir, source } of dirs) {
|
|
155
164
|
try {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
+
// Check both patterns in ALL skill directories:
|
|
166
|
+
// 1. Flat files: {dir}/{name}.md
|
|
167
|
+
// 2. Subdirectories: {dir}/{name}/SKILL.md (ecosystem standard)
|
|
168
|
+
const flatFiles = await glob('*.md', { cwd: dir, absolute: true })
|
|
169
|
+
const subdirFiles = await glob('*/SKILL.md', { cwd: dir, absolute: true })
|
|
170
|
+
|
|
171
|
+
// Load subdirectory skills first (ecosystem standard takes priority within same dir)
|
|
172
|
+
for (const file of subdirFiles) {
|
|
173
|
+
const skill = await this.loadSkill(file, source)
|
|
174
|
+
if (skill && !this.skills.has(skill.id)) {
|
|
175
|
+
this.skills.set(skill.id, skill)
|
|
165
176
|
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
this.skills.set(skill.id, skill)
|
|
174
|
-
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Then flat files (don't override subdirectory version)
|
|
180
|
+
for (const file of flatFiles) {
|
|
181
|
+
const skill = await this.loadSkill(file, source)
|
|
182
|
+
if (skill && !this.skills.has(skill.id)) {
|
|
183
|
+
this.skills.set(skill.id, skill)
|
|
175
184
|
}
|
|
176
185
|
}
|
|
177
186
|
} catch (_error) {
|
|
@@ -256,6 +265,7 @@ class SkillService {
|
|
|
256
265
|
project: [],
|
|
257
266
|
global: [],
|
|
258
267
|
builtin: [],
|
|
268
|
+
remote: [],
|
|
259
269
|
}
|
|
260
270
|
|
|
261
271
|
for (const skill of skills) {
|
package/core/types/provider.ts
CHANGED
|
@@ -5,19 +5,22 @@
|
|
|
5
5
|
* - Claude Code (CLI)
|
|
6
6
|
* - Gemini CLI (CLI)
|
|
7
7
|
* - Cursor IDE (GUI, project-level config)
|
|
8
|
+
* - Windsurf IDE (GUI, project-level config)
|
|
8
9
|
*
|
|
9
10
|
* Key discovery: Skills use identical SKILL.md format for CLI providers.
|
|
10
11
|
* Cursor uses .mdc files with frontmatter for rules.
|
|
12
|
+
* Windsurf uses .md files with YAML frontmatter for rules.
|
|
11
13
|
*
|
|
12
14
|
* @see https://geminicli.com/docs/cli/gemini-md/
|
|
13
15
|
* @see https://geminicli.com/docs/cli/skills/
|
|
14
16
|
* @see https://cursor.com/docs/context/rules
|
|
17
|
+
* @see https://docs.windsurf.com/windsurf/cascade/memories
|
|
15
18
|
*/
|
|
16
19
|
|
|
17
20
|
/**
|
|
18
21
|
* Supported AI provider names
|
|
19
22
|
*/
|
|
20
|
-
export type AIProviderName = 'claude' | 'gemini' | 'cursor'
|
|
23
|
+
export type AIProviderName = 'claude' | 'gemini' | 'cursor' | 'antigravity' | 'windsurf'
|
|
21
24
|
|
|
22
25
|
/**
|
|
23
26
|
* Command format for each provider
|
|
@@ -122,6 +125,20 @@ export interface CursorProjectDetection {
|
|
|
122
125
|
projectRoot?: string
|
|
123
126
|
}
|
|
124
127
|
|
|
128
|
+
/**
|
|
129
|
+
* Result of Windsurf project detection
|
|
130
|
+
*/
|
|
131
|
+
export interface WindsurfProjectDetection {
|
|
132
|
+
/** Whether .windsurf/ directory exists in project */
|
|
133
|
+
detected: boolean
|
|
134
|
+
|
|
135
|
+
/** Whether prjct router is installed */
|
|
136
|
+
routerInstalled: boolean
|
|
137
|
+
|
|
138
|
+
/** Project root path */
|
|
139
|
+
projectRoot?: string
|
|
140
|
+
}
|
|
141
|
+
|
|
125
142
|
/**
|
|
126
143
|
* Provider-aware branding configuration
|
|
127
144
|
*/
|
package/core/types/services.ts
CHANGED
|
@@ -28,6 +28,11 @@ export interface SkillMetadata {
|
|
|
28
28
|
version?: string
|
|
29
29
|
category?: string
|
|
30
30
|
author?: string
|
|
31
|
+
// Ecosystem compatibility fields
|
|
32
|
+
sourceUrl?: string
|
|
33
|
+
sourceType?: 'github' | 'local' | 'builtin' | 'registry'
|
|
34
|
+
installedAt?: string
|
|
35
|
+
sha?: string
|
|
31
36
|
}
|
|
32
37
|
|
|
33
38
|
export interface Skill {
|
|
@@ -35,7 +40,7 @@ export interface Skill {
|
|
|
35
40
|
name: string
|
|
36
41
|
description: string
|
|
37
42
|
content: string
|
|
38
|
-
source: 'project' | 'global' | 'builtin'
|
|
43
|
+
source: 'project' | 'global' | 'builtin' | 'remote'
|
|
39
44
|
filePath: string
|
|
40
45
|
metadata: SkillMetadata
|
|
41
46
|
path?: string
|