@plainconceptsplatform/agent-harness 3.8.2 → 3.8.3
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/LICENSE +21 -21
- package/README.md +551 -549
- package/cli/commands/join.js +259 -259
- package/cli/commands/migrate.js +310 -310
- package/cli/commands/shared.js +27 -27
- package/cli/commands/single.js +79 -79
- package/cli/commands/update.js +121 -121
- package/cli/commands/wizard.js +134 -134
- package/cli/fragments/archive/az.md +97 -97
- package/cli/fragments/archive/gh.md +96 -96
- package/cli/fragments/archive/gl.md +96 -96
- package/cli/fragments/archive/none.md +75 -75
- package/cli/fragments/guardrails/codegraph.md +5 -5
- package/cli/fragments/guardrails/humanizer.md +4 -4
- package/cli/fragments/guardrails/memory.md +4 -4
- package/cli/fragments/guardrails/rtk.md +3 -3
- package/cli/fragments/guardrails/simple-english.md +4 -4
- package/cli/fragments/ops-backlog/az.md +28 -28
- package/cli/fragments/ops-backlog/gh.md +28 -28
- package/cli/fragments/ops-backlog/jira.md +28 -28
- package/cli/fragments/ops-review/az.md +62 -62
- package/cli/fragments/ops-review/gh.md +51 -51
- package/cli/fragments/ops-review/gl.md +55 -55
- package/cli/fragments/ops-ship/az.md +81 -81
- package/cli/fragments/ops-ship/gh.md +67 -67
- package/cli/fragments/ops-ship/gl.md +84 -84
- package/cli/index.js +116 -116
- package/cli/presets/agents-content.json +34 -34
- package/cli/presets/browser.json +10 -10
- package/cli/presets/clean.json +21 -21
- package/cli/presets/models.json +68 -68
- package/cli/presets/optimization.json +32 -32
- package/cli/presets/platforms.json +76 -76
- package/cli/presets/source.json +23 -23
- package/cli/steps/browser/index.js +48 -48
- package/cli/steps/clean/index.js +120 -120
- package/cli/steps/copy/agents.js +176 -160
- package/cli/steps/copy/commands.js +79 -79
- package/cli/steps/copy/fullstack-engineer.js +89 -89
- package/cli/steps/copy/index.js +88 -88
- package/cli/steps/copy/opencode-json.js +324 -324
- package/cli/steps/copy/skills.js +316 -316
- package/cli/steps/metadata/index.js +115 -115
- package/cli/steps/models/format.js +88 -88
- package/cli/steps/models/index.js +64 -64
- package/cli/steps/models/write.js +38 -38
- package/cli/steps/openspec/index.js +136 -136
- package/cli/steps/optimization/codegraph.js +141 -141
- package/cli/steps/optimization/detect.js +66 -66
- package/cli/steps/optimization/humanizer.js +17 -17
- package/cli/steps/optimization/index.js +158 -158
- package/cli/steps/optimization/memory.js +91 -91
- package/cli/steps/optimization/patch-guardrails.js +111 -111
- package/cli/steps/optimization/simple-english.js +17 -17
- package/cli/steps/optimization/skills-lock.js +30 -30
- package/cli/steps/platform/index.js +109 -109
- package/cli/steps/source/index.js +123 -123
- package/cli/utils/agent-color.js +111 -111
- package/cli/utils/copy.js +134 -134
- package/cli/utils/exec-spinner.js +47 -47
- package/cli/utils/exec.js +134 -134
- package/cli/utils/legacy-check.js +30 -30
- package/cli/utils/models-cache.js +58 -58
- package/cli/utils/models-pricing.js +42 -42
- package/cli/utils/paths.js +82 -82
- package/cli/utils/process.js +3 -3
- package/cli/utils/terminal.js +6 -6
- package/cli/utils/update-manifest.js +75 -75
- package/harness/.opencode/_gitignore +10 -10
- package/harness/.opencode/commands/init.md +5 -5
- package/harness/.opencode/commands/make-architecture.md +5 -5
- package/harness/.opencode/commands/make-design.md +5 -5
- package/harness/.opencode/commands/make-engineer.md +5 -5
- package/harness/.opencode/commands/make-guardrails.md +5 -5
- package/harness/.opencode/commands/make-user-model.md +5 -5
- package/harness/.opencode/commands/ops-backlog.md +10 -10
- package/harness/.opencode/commands/ops-review.md +10 -10
- package/harness/.opencode/commands/ops-ship.md +9 -9
- package/harness/.opencode/commands/plan-apply.md +9 -9
- package/harness/.opencode/commands/plan-archive.md +7 -7
- package/harness/.opencode/commands/plan-explore.md +11 -11
- package/harness/.opencode/commands/plan-goal.md +5 -5
- package/harness/.opencode/commands/plan-propose.md +11 -11
- package/harness/.opencode/commands/plan-quick.md +7 -7
- package/harness/.opencode/commands/plan-story.md +11 -11
- package/harness/.opencode/commands/repo-audit.md +7 -7
- package/harness/.opencode/commands/repo-help.md +5 -5
- package/harness/.opencode/commands/repo-initialize.md +5 -5
- package/harness/.opencode/commands/repo-onboard.md +5 -5
- package/harness/.opencode/commands/repo-verify.md +5 -5
- package/harness/.opencode/package.json +5 -5
- package/harness/.opencode/plugins/pc-commit-coauthor.js +41 -41
- package/harness/.opencode/plugins/pc-subagent-monitor.js +184 -184
- package/harness/.opencode/plugins/pc-subagent-tiers.js +367 -367
- package/harness/.opencode/plugins/pc-system-reminders.js +424 -424
- package/harness/.opencode/scripts/check-diff.mjs +305 -305
- package/harness/.opencode/scripts/code-rules.mjs +288 -288
- package/harness/.opencode/scripts/platform-contract.mjs +366 -366
- package/harness/AGENTS.md +55 -49
- package/harness/ARCHITECTURE.md +13 -13
- package/harness/DESIGN.md +13 -13
- package/harness/opencode.jsonc +36 -36
- package/harness/openspec/config.yaml +20 -20
- package/harness/skills-lock.json +17 -17
- package/package.json +12 -14
- package/skills/README.md +18 -18
- package/skills/agent-harness-cli/SKILL.md +86 -86
- package/skills/browser-automation/SKILL.md +72 -72
- package/skills/pc-guardrails-generic/SKILL.md +53 -53
- package/skills/pc-guardrails-project/SKILL.md +8 -8
- package/skills/pc-make-architecture/SKILL.md +33 -33
- package/skills/pc-make-architecture/structure-template.md +64 -64
- package/skills/pc-make-design/SKILL.md +45 -45
- package/skills/pc-make-engineer/SKILL.md +59 -59
- package/skills/pc-make-engineer/signal-mapping.md +53 -53
- package/skills/pc-make-engineer/template.md +36 -36
- package/skills/pc-make-guardrails/SKILL.md +44 -44
- package/skills/pc-make-guardrails/category-reference.md +112 -112
- package/skills/pc-make-merge-risk-assess/SKILL.md +70 -70
- package/skills/pc-make-merge-risk-assess/category-reference.md +114 -114
- package/skills/pc-make-user-model/SKILL.md +56 -56
- package/skills/pc-ops-ship/SKILL.md +22 -22
- package/skills/pc-plan-apply/SKILL.md +90 -90
- package/skills/pc-plan-apply/simple-mode.md +21 -21
- package/skills/pc-plan-archive/SKILL.md +66 -66
- package/skills/pc-plan-explore/SKILL.md +21 -21
- package/skills/pc-plan-goal/SKILL.md +91 -91
- package/skills/pc-plan-goal/branching.md +39 -39
- package/skills/pc-plan-goal/failure-policy.md +30 -30
- package/skills/pc-plan-goal/output-mode.md +10 -10
- package/skills/pc-plan-goal/output.md +71 -71
- package/skills/pc-plan-propose/SKILL.md +127 -127
- package/skills/pc-plan-propose/task-annotation.md +39 -39
- package/skills/pc-plan-quick/SKILL.md +46 -46
- package/skills/pc-plan-story/SKILL.md +93 -93
- package/skills/pc-repo-audit/SKILL.md +44 -44
- package/skills/pc-repo-help/SKILL.md +87 -87
- package/skills/pc-repo-initialize/SKILL.md +119 -119
- package/skills/pc-repo-onboard/SKILL.md +32 -32
- package/skills/pc-repo-verify/SKILL.md +150 -150
- package/skills/pc-userstory-az/SKILL.md +71 -71
- package/skills/pc-userstory-browser/SKILL.md +64 -64
- package/skills/pc-userstory-gh/SKILL.md +63 -63
- package/skills/pc-userstory-jira/SKILL.md +74 -74
package/cli/steps/copy/skills.js
CHANGED
|
@@ -1,316 +1,316 @@
|
|
|
1
|
-
import fse from 'fs-extra'
|
|
2
|
-
import path from 'path'
|
|
3
|
-
import { fileURLToPath } from 'url'
|
|
4
|
-
import { info, success } from '../../utils/exec.js'
|
|
5
|
-
import { canUpdateManagedFile, readUpdateManifest, recordManagedFile, writeUpdateManifest } from '../../utils/update-manifest.js'
|
|
6
|
-
import { RETIRED_PATHS } from '../../utils/paths.js'
|
|
7
|
-
|
|
8
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
9
|
-
const CONTENT_SKILLS_DIR = path.resolve(__dirname, '../../../skills')
|
|
10
|
-
const CONTENT_SKILLS_LOCK = path.resolve(__dirname, '../../../harness/skills-lock.json')
|
|
11
|
-
|
|
12
|
-
// Userstory skills parse backlog work items: selected by backlogPlatform only.
|
|
13
|
-
// Mixing the two axes here installs the wrong variant on mixed setups, because
|
|
14
|
-
// all variants rename to the same generic dir and the first copy wins.
|
|
15
|
-
const BACKLOG_PLATFORM_SKILLS = {
|
|
16
|
-
'pc-userstory-gh': 'github',
|
|
17
|
-
'pc-userstory-az': 'azure',
|
|
18
|
-
'pc-userstory-jira': 'jira',
|
|
19
|
-
'pc-userstory-browser': 'browser',
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
// Platform-specific skills are renamed to their generic form on install.
|
|
23
|
-
// The -gh / -az / -jira / -gl suffix is only needed here to keep all variants in source.
|
|
24
|
-
// After install only one platform is present so no suffix is needed.
|
|
25
|
-
export const SKILL_RENAME = {
|
|
26
|
-
'pc-userstory-gh': 'pc-userstory',
|
|
27
|
-
'pc-userstory-az': 'pc-userstory',
|
|
28
|
-
'pc-userstory-jira': 'pc-userstory',
|
|
29
|
-
'pc-userstory-browser': 'pc-userstory',
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// Skills that ship in the source tree but must never be copied into a target
|
|
33
|
-
// project. agent-harness-cli documents the CLI itself for agents working on
|
|
34
|
-
// THIS repo; it has no value in a consumer repo and would clutter .agents/skills.
|
|
35
|
-
// Listed by directory name as read from CONTENT_SKILLS_DIR.
|
|
36
|
-
export const NON_INSTALLABLE_SKILLS = new Set([
|
|
37
|
-
'agent-harness-cli',
|
|
38
|
-
])
|
|
39
|
-
|
|
40
|
-
function shouldInstallSkill(skill, backlogPlatform, _repoPlatform) {
|
|
41
|
-
if (NON_INSTALLABLE_SKILLS.has(skill)) return false
|
|
42
|
-
if (skill in BACKLOG_PLATFORM_SKILLS) return BACKLOG_PLATFORM_SKILLS[skill] === backlogPlatform
|
|
43
|
-
return true
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// Shipped skills whose content is a PLACEHOLDER — a /make-* command
|
|
47
|
-
// overwrites them with project-specific content. During forceOverwrite,
|
|
48
|
-
// these must be preserved when they have already been generated (detected
|
|
49
|
-
// by the <!-- Last updated: marker). Without this check, the update would
|
|
50
|
-
// wipe project-specific guardrails, risk assessments, etc.
|
|
51
|
-
export const GENERATABLE_SKILLS = new Set([
|
|
52
|
-
'pc-guardrails-project',
|
|
53
|
-
'pc-merge-risk-assess',
|
|
54
|
-
])
|
|
55
|
-
|
|
56
|
-
const MARKER_SKILLS = new Set([
|
|
57
|
-
'pc-guardrails-generic',
|
|
58
|
-
'pc-plan-archive',
|
|
59
|
-
'pc-ops-ship',
|
|
60
|
-
'pc-repo-initialize',
|
|
61
|
-
])
|
|
62
|
-
|
|
63
|
-
// Marker skills whose prose must track the shipped version on update.
|
|
64
|
-
//
|
|
65
|
-
// These are shipped content, not project content: the project-specific part is
|
|
66
|
-
// injected into their marker pairs, and the platform patchers plus
|
|
67
|
-
// patchGuardrails re-inject it immediately after this copy. Treating them as
|
|
68
|
-
// project-owned froze their prose at whatever version a consumer first
|
|
69
|
-
// installed, so a skill rewrite never reached an existing repo. That is also
|
|
70
|
-
// how browser-automation sat on v1.0 in five repos while the harness shipped
|
|
71
|
-
// v2.0.
|
|
72
|
-
//
|
|
73
|
-
// pc-repo-initialize is deliberately absent. patchAgentsMd stamps "skipped
|
|
74
|
-
// during onboarding" notes into it that only run on a fresh install
|
|
75
|
-
// (copy/index.js runs it under `!ctx.updateMode`), so refreshing it would
|
|
76
|
-
// discard decisions made at onboarding.
|
|
77
|
-
const REFRESHABLE_MARKER_SKILLS = new Set([
|
|
78
|
-
'pc-guardrails-generic',
|
|
79
|
-
'pc-plan-archive',
|
|
80
|
-
'pc-ops-ship',
|
|
81
|
-
])
|
|
82
|
-
|
|
83
|
-
// A project-owned slot inside a shipped skill: the content between the markers
|
|
84
|
-
// survives a refresh, everything around it takes the new shipped version. This
|
|
85
|
-
// is the supported way to adapt one example to a project without forking the
|
|
86
|
-
// file, which consumers have otherwise done by editing shipped prose in place.
|
|
87
|
-
const PROJECT_SLOT_RE = /<!-- (PC-PROJECT-[A-Z0-9-]+)-START -->([\s\S]*?)<!-- \1-END -->/g
|
|
88
|
-
// Separate, non-global: `.test()` on a /g/ regex advances lastIndex, so reusing
|
|
89
|
-
// PROJECT_SLOT_RE for the check would answer false on every other call.
|
|
90
|
-
const HAS_PROJECT_SLOT = /<!-- PC-PROJECT-[A-Z0-9-]+-START -->/
|
|
91
|
-
|
|
92
|
-
function readProjectSlots(text) {
|
|
93
|
-
const slots = new Map()
|
|
94
|
-
for (const match of text.matchAll(PROJECT_SLOT_RE)) slots.set(match[1], match[2])
|
|
95
|
-
return slots
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function writeProjectSlots(text, slots) {
|
|
99
|
-
if (slots.size === 0) return text
|
|
100
|
-
// Replacer function, not a replacement string: carried content can contain
|
|
101
|
-
// shell quoting like $'...', and a string replacement expands $' as
|
|
102
|
-
// "everything after the match" and truncates the file.
|
|
103
|
-
return text.replace(PROJECT_SLOT_RE, (whole, name) => {
|
|
104
|
-
const kept = slots.get(name)
|
|
105
|
-
if (kept === undefined || kept.trim() === '') return whole
|
|
106
|
-
return `<!-- ${name}-START -->${kept}<!-- ${name}-END -->`
|
|
107
|
-
})
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
async function refreshMarkerSkill(src, dest, relativeRoot) {
|
|
111
|
-
for (const entry of await fse.readdir(src, { withFileTypes: true })) {
|
|
112
|
-
const sourcePath = path.join(src, entry.name)
|
|
113
|
-
const destinationPath = path.join(dest, entry.name)
|
|
114
|
-
const relativePath = path.join(relativeRoot, entry.name)
|
|
115
|
-
if (entry.isDirectory()) {
|
|
116
|
-
await refreshMarkerSkill(sourcePath, destinationPath, relativePath)
|
|
117
|
-
continue
|
|
118
|
-
}
|
|
119
|
-
const shipped = await fse.readFile(sourcePath, 'utf-8')
|
|
120
|
-
const existing = await fse.pathExists(destinationPath)
|
|
121
|
-
? await fse.readFile(destinationPath, 'utf-8')
|
|
122
|
-
: ''
|
|
123
|
-
const merged = writeProjectSlots(shipped, readProjectSlots(existing))
|
|
124
|
-
if (merged === existing) continue
|
|
125
|
-
await fse.ensureDir(path.dirname(destinationPath))
|
|
126
|
-
await fse.writeFile(destinationPath, merged, 'utf-8')
|
|
127
|
-
success(`Refreshed skill: ${relativePath}`)
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
async function isGeneratedSkill(dest) {
|
|
132
|
-
const skillMd = path.join(dest, 'SKILL.md')
|
|
133
|
-
if (!await fse.pathExists(skillMd)) return false
|
|
134
|
-
const content = await fse.readFile(skillMd, 'utf-8')
|
|
135
|
-
return content.includes('<!-- Last updated:')
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
async function syncSkillFiles(src, dest, relativeRoot, cwd, manifest) {
|
|
139
|
-
for (const entry of await fse.readdir(src, { withFileTypes: true })) {
|
|
140
|
-
const sourcePath = path.join(src, entry.name)
|
|
141
|
-
const destinationPath = path.join(dest, entry.name)
|
|
142
|
-
const relativePath = path.join(relativeRoot, entry.name)
|
|
143
|
-
if (entry.isDirectory()) {
|
|
144
|
-
await syncSkillFiles(sourcePath, destinationPath, relativePath, cwd, manifest)
|
|
145
|
-
continue
|
|
146
|
-
}
|
|
147
|
-
const exists = await fse.pathExists(destinationPath)
|
|
148
|
-
if (!exists || await canUpdateManagedFile(relativePath, cwd, manifest)) {
|
|
149
|
-
await fse.ensureDir(path.dirname(destinationPath))
|
|
150
|
-
// The managed check ignores what sits between markers, so an untouched file can still hold
|
|
151
|
-
// a filled PC-PROJECT-* slot; copying the source over it erased Numa's sign-in steps.
|
|
152
|
-
const shipped = await fse.readFile(sourcePath, 'utf-8')
|
|
153
|
-
if (exists && HAS_PROJECT_SLOT.test(shipped)) {
|
|
154
|
-
const existing = await fse.readFile(destinationPath, 'utf-8')
|
|
155
|
-
await fse.writeFile(destinationPath, writeProjectSlots(shipped, readProjectSlots(existing)), 'utf-8')
|
|
156
|
-
} else {
|
|
157
|
-
await fse.copyFile(sourcePath, destinationPath)
|
|
158
|
-
}
|
|
159
|
-
await recordManagedFile(manifest, relativePath, sourcePath)
|
|
160
|
-
success(`Updated skill: ${relativePath}`)
|
|
161
|
-
continue
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// A shipped file that declares a PC-PROJECT-* slot states in its own prose
|
|
165
|
-
// that the slot is the project's and everything else is the harness's, so a
|
|
166
|
-
// hand-edited copy still takes the update: merge rather than preserve.
|
|
167
|
-
// Without this the file is frozen at whatever version the project edited,
|
|
168
|
-
// which is how five repos ended up with stale skills.
|
|
169
|
-
const shipped = await fse.readFile(sourcePath, 'utf-8')
|
|
170
|
-
if (HAS_PROJECT_SLOT.test(shipped)) {
|
|
171
|
-
const existing = await fse.readFile(destinationPath, 'utf-8')
|
|
172
|
-
const merged = writeProjectSlots(shipped, readProjectSlots(existing))
|
|
173
|
-
if (merged !== existing) {
|
|
174
|
-
await fse.writeFile(destinationPath, merged, 'utf-8')
|
|
175
|
-
success(`Refreshed skill, project slot kept: ${relativePath}`)
|
|
176
|
-
}
|
|
177
|
-
continue
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
info(`Preserving modified skill file: ${relativePath}`)
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
async function recordSkillSourceFiles(src, relativeRoot, manifest) {
|
|
185
|
-
for (const entry of await fse.readdir(src, { withFileTypes: true })) {
|
|
186
|
-
const sourcePath = path.join(src, entry.name)
|
|
187
|
-
const relativePath = path.join(relativeRoot, entry.name)
|
|
188
|
-
if (entry.isDirectory()) {
|
|
189
|
-
await recordSkillSourceFiles(sourcePath, relativePath, manifest)
|
|
190
|
-
} else {
|
|
191
|
-
await recordManagedFile(manifest, relativePath, sourcePath)
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
async function installObSkills(backlogPlatform = 'github', repoPlatform, { forceOverwrite = false, updateMode = false } = {}) {
|
|
197
|
-
const repo = repoPlatform ?? backlogPlatform
|
|
198
|
-
const destSkillsDir = path.join(process.cwd(), '.agents', 'skills')
|
|
199
|
-
await fse.ensureDir(destSkillsDir)
|
|
200
|
-
const manifest = await readUpdateManifest()
|
|
201
|
-
|
|
202
|
-
// Build the set of skill names we ship (source dirs, after rename).
|
|
203
|
-
// Only these may be removed during forceOverwrite — project-generated
|
|
204
|
-
// skills (pc-merge-risk-assess, custom loops, etc.) must survive.
|
|
205
|
-
const contentSkills = (await fse.readdir(CONTENT_SKILLS_DIR))
|
|
206
|
-
.filter(name => !NON_INSTALLABLE_SKILLS.has(name))
|
|
207
|
-
const shippedNames = new Set()
|
|
208
|
-
for (const skill of contentSkills) {
|
|
209
|
-
const stat = await fse.stat(path.join(CONTENT_SKILLS_DIR, skill)).catch(() => null)
|
|
210
|
-
if (stat?.isDirectory()) shippedNames.add(SKILL_RENAME[skill] ?? skill)
|
|
211
|
-
}
|
|
212
|
-
// Also account for stale platform-variant names (e.g. pc-userstory-gh
|
|
213
|
-
// when the current platform is azure).
|
|
214
|
-
Object.keys(SKILL_RENAME).forEach(k => shippedNames.add(k))
|
|
215
|
-
|
|
216
|
-
if (forceOverwrite && !updateMode) {
|
|
217
|
-
for (const entry of await fse.readdir(destSkillsDir)) {
|
|
218
|
-
if (!entry.startsWith('pc-')) continue
|
|
219
|
-
if (!shippedNames.has(entry)) {
|
|
220
|
-
info(`Preserving project-generated skill: ${entry}`)
|
|
221
|
-
continue
|
|
222
|
-
}
|
|
223
|
-
// Generatable skills: preserve if already populated by /make-*
|
|
224
|
-
if (GENERATABLE_SKILLS.has(entry) && await isGeneratedSkill(path.join(destSkillsDir, entry))) {
|
|
225
|
-
info(`Preserving generated skill: ${entry}`)
|
|
226
|
-
continue
|
|
227
|
-
}
|
|
228
|
-
await fse.remove(path.join(destSkillsDir, entry))
|
|
229
|
-
info(`Removing shipped skill: ${entry}`)
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
const skills = contentSkills
|
|
234
|
-
for (const skill of skills) {
|
|
235
|
-
const src = path.join(CONTENT_SKILLS_DIR, skill)
|
|
236
|
-
const destName = SKILL_RENAME[skill] ?? skill
|
|
237
|
-
const dest = path.join(destSkillsDir, destName)
|
|
238
|
-
const stat = await fse.stat(src)
|
|
239
|
-
if (!stat.isDirectory()) continue
|
|
240
|
-
if (!shouldInstallSkill(skill, backlogPlatform, repo)) {
|
|
241
|
-
if (forceOverwrite) {
|
|
242
|
-
const staleDest = path.join(destSkillsDir, skill)
|
|
243
|
-
if (skill !== destName && await fse.pathExists(staleDest)) {
|
|
244
|
-
await fse.remove(staleDest)
|
|
245
|
-
info(`Removing stale skill: ${skill}`)
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
info(`Skipping skill: ${skill} (not needed for platforms: ${backlogPlatform}/${repo})`)
|
|
249
|
-
continue
|
|
250
|
-
}
|
|
251
|
-
if (updateMode) {
|
|
252
|
-
const relativeRoot = path.join('.agents', 'skills', destName)
|
|
253
|
-
if (!await fse.pathExists(dest)) {
|
|
254
|
-
await syncSkillFiles(src, dest, relativeRoot, process.cwd(), manifest)
|
|
255
|
-
} else if (GENERATABLE_SKILLS.has(destName)) {
|
|
256
|
-
info(`Preserving generated skill: ${destName}`)
|
|
257
|
-
} else if (REFRESHABLE_MARKER_SKILLS.has(destName)) {
|
|
258
|
-
// Shipped prose refreshes; the patchers re-inject the marker pairs and
|
|
259
|
-
// any PC-PROJECT-* slot is carried over.
|
|
260
|
-
await refreshMarkerSkill(src, dest, relativeRoot)
|
|
261
|
-
} else if (MARKER_SKILLS.has(destName)) {
|
|
262
|
-
info(`Preserving project-owned skill: ${destName}`)
|
|
263
|
-
} else {
|
|
264
|
-
await syncSkillFiles(src, dest, relativeRoot, process.cwd(), manifest)
|
|
265
|
-
}
|
|
266
|
-
continue
|
|
267
|
-
}
|
|
268
|
-
if (await fse.pathExists(dest) && !forceOverwrite) {
|
|
269
|
-
info(`${destName} already exists, skipping`)
|
|
270
|
-
continue
|
|
271
|
-
}
|
|
272
|
-
// Even with forceOverwrite, never overwrite an already-generated
|
|
273
|
-
// generatable skill — its content was produced by /make-guardrails or
|
|
274
|
-
// similar, not by the shipped placeholder.
|
|
275
|
-
if (forceOverwrite && GENERATABLE_SKILLS.has(destName) && await isGeneratedSkill(dest)) {
|
|
276
|
-
info(`Preserving generated skill: ${destName}`)
|
|
277
|
-
continue
|
|
278
|
-
}
|
|
279
|
-
await fse.copy(src, dest, { overwrite: true })
|
|
280
|
-
if (!MARKER_SKILLS.has(destName) && !GENERATABLE_SKILLS.has(destName)) {
|
|
281
|
-
await recordSkillSourceFiles(src, path.join('.agents', 'skills', destName), manifest)
|
|
282
|
-
}
|
|
283
|
-
success(`${forceOverwrite ? 'Updated' : 'Installed'} skill: ${destName}`)
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
await writeUpdateManifest(manifest)
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
export async function installSkills(backlogPlatform = 'github', repoPlatform, opts = {}) {
|
|
290
|
-
info('Installing built-in pc-skills...')
|
|
291
|
-
await installObSkills(backlogPlatform, repoPlatform, opts)
|
|
292
|
-
console.log()
|
|
293
|
-
|
|
294
|
-
if (await fse.pathExists(CONTENT_SKILLS_LOCK)) {
|
|
295
|
-
const destLock = path.join(process.cwd(), 'skills-lock.json')
|
|
296
|
-
if (await fse.pathExists(destLock) && (opts.updateMode || !opts.forceOverwrite)) {
|
|
297
|
-
info('skills-lock.json already exists, skipping')
|
|
298
|
-
} else {
|
|
299
|
-
await fse.copy(CONTENT_SKILLS_LOCK, destLock, { overwrite: true })
|
|
300
|
-
success(`${opts.forceOverwrite ? 'Updated' : 'Installed'} skills-lock.json`)
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
// npx skills experimental_install is now called once at the end of the
|
|
305
|
-
// optimization step, after the user has selected which tools to enable.
|
|
306
|
-
// This avoids multiple npx skills add calls during onboarding.
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
export async function removeRetiredFiles(cwd = process.cwd()) {
|
|
310
|
-
for (const relative of RETIRED_PATHS) {
|
|
311
|
-
const target = path.join(cwd, relative)
|
|
312
|
-
if (!await fse.pathExists(target)) continue
|
|
313
|
-
await fse.remove(target)
|
|
314
|
-
info(`Removed retired: ${relative}`)
|
|
315
|
-
}
|
|
316
|
-
}
|
|
1
|
+
import fse from 'fs-extra'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import { fileURLToPath } from 'url'
|
|
4
|
+
import { info, success } from '../../utils/exec.js'
|
|
5
|
+
import { canUpdateManagedFile, readUpdateManifest, recordManagedFile, writeUpdateManifest } from '../../utils/update-manifest.js'
|
|
6
|
+
import { RETIRED_PATHS } from '../../utils/paths.js'
|
|
7
|
+
|
|
8
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
9
|
+
const CONTENT_SKILLS_DIR = path.resolve(__dirname, '../../../skills')
|
|
10
|
+
const CONTENT_SKILLS_LOCK = path.resolve(__dirname, '../../../harness/skills-lock.json')
|
|
11
|
+
|
|
12
|
+
// Userstory skills parse backlog work items: selected by backlogPlatform only.
|
|
13
|
+
// Mixing the two axes here installs the wrong variant on mixed setups, because
|
|
14
|
+
// all variants rename to the same generic dir and the first copy wins.
|
|
15
|
+
const BACKLOG_PLATFORM_SKILLS = {
|
|
16
|
+
'pc-userstory-gh': 'github',
|
|
17
|
+
'pc-userstory-az': 'azure',
|
|
18
|
+
'pc-userstory-jira': 'jira',
|
|
19
|
+
'pc-userstory-browser': 'browser',
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Platform-specific skills are renamed to their generic form on install.
|
|
23
|
+
// The -gh / -az / -jira / -gl suffix is only needed here to keep all variants in source.
|
|
24
|
+
// After install only one platform is present so no suffix is needed.
|
|
25
|
+
export const SKILL_RENAME = {
|
|
26
|
+
'pc-userstory-gh': 'pc-userstory',
|
|
27
|
+
'pc-userstory-az': 'pc-userstory',
|
|
28
|
+
'pc-userstory-jira': 'pc-userstory',
|
|
29
|
+
'pc-userstory-browser': 'pc-userstory',
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Skills that ship in the source tree but must never be copied into a target
|
|
33
|
+
// project. agent-harness-cli documents the CLI itself for agents working on
|
|
34
|
+
// THIS repo; it has no value in a consumer repo and would clutter .agents/skills.
|
|
35
|
+
// Listed by directory name as read from CONTENT_SKILLS_DIR.
|
|
36
|
+
export const NON_INSTALLABLE_SKILLS = new Set([
|
|
37
|
+
'agent-harness-cli',
|
|
38
|
+
])
|
|
39
|
+
|
|
40
|
+
function shouldInstallSkill(skill, backlogPlatform, _repoPlatform) {
|
|
41
|
+
if (NON_INSTALLABLE_SKILLS.has(skill)) return false
|
|
42
|
+
if (skill in BACKLOG_PLATFORM_SKILLS) return BACKLOG_PLATFORM_SKILLS[skill] === backlogPlatform
|
|
43
|
+
return true
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Shipped skills whose content is a PLACEHOLDER — a /make-* command
|
|
47
|
+
// overwrites them with project-specific content. During forceOverwrite,
|
|
48
|
+
// these must be preserved when they have already been generated (detected
|
|
49
|
+
// by the <!-- Last updated: marker). Without this check, the update would
|
|
50
|
+
// wipe project-specific guardrails, risk assessments, etc.
|
|
51
|
+
export const GENERATABLE_SKILLS = new Set([
|
|
52
|
+
'pc-guardrails-project',
|
|
53
|
+
'pc-merge-risk-assess',
|
|
54
|
+
])
|
|
55
|
+
|
|
56
|
+
const MARKER_SKILLS = new Set([
|
|
57
|
+
'pc-guardrails-generic',
|
|
58
|
+
'pc-plan-archive',
|
|
59
|
+
'pc-ops-ship',
|
|
60
|
+
'pc-repo-initialize',
|
|
61
|
+
])
|
|
62
|
+
|
|
63
|
+
// Marker skills whose prose must track the shipped version on update.
|
|
64
|
+
//
|
|
65
|
+
// These are shipped content, not project content: the project-specific part is
|
|
66
|
+
// injected into their marker pairs, and the platform patchers plus
|
|
67
|
+
// patchGuardrails re-inject it immediately after this copy. Treating them as
|
|
68
|
+
// project-owned froze their prose at whatever version a consumer first
|
|
69
|
+
// installed, so a skill rewrite never reached an existing repo. That is also
|
|
70
|
+
// how browser-automation sat on v1.0 in five repos while the harness shipped
|
|
71
|
+
// v2.0.
|
|
72
|
+
//
|
|
73
|
+
// pc-repo-initialize is deliberately absent. patchAgentsMd stamps "skipped
|
|
74
|
+
// during onboarding" notes into it that only run on a fresh install
|
|
75
|
+
// (copy/index.js runs it under `!ctx.updateMode`), so refreshing it would
|
|
76
|
+
// discard decisions made at onboarding.
|
|
77
|
+
const REFRESHABLE_MARKER_SKILLS = new Set([
|
|
78
|
+
'pc-guardrails-generic',
|
|
79
|
+
'pc-plan-archive',
|
|
80
|
+
'pc-ops-ship',
|
|
81
|
+
])
|
|
82
|
+
|
|
83
|
+
// A project-owned slot inside a shipped skill: the content between the markers
|
|
84
|
+
// survives a refresh, everything around it takes the new shipped version. This
|
|
85
|
+
// is the supported way to adapt one example to a project without forking the
|
|
86
|
+
// file, which consumers have otherwise done by editing shipped prose in place.
|
|
87
|
+
const PROJECT_SLOT_RE = /<!-- (PC-PROJECT-[A-Z0-9-]+)-START -->([\s\S]*?)<!-- \1-END -->/g
|
|
88
|
+
// Separate, non-global: `.test()` on a /g/ regex advances lastIndex, so reusing
|
|
89
|
+
// PROJECT_SLOT_RE for the check would answer false on every other call.
|
|
90
|
+
const HAS_PROJECT_SLOT = /<!-- PC-PROJECT-[A-Z0-9-]+-START -->/
|
|
91
|
+
|
|
92
|
+
function readProjectSlots(text) {
|
|
93
|
+
const slots = new Map()
|
|
94
|
+
for (const match of text.matchAll(PROJECT_SLOT_RE)) slots.set(match[1], match[2])
|
|
95
|
+
return slots
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function writeProjectSlots(text, slots) {
|
|
99
|
+
if (slots.size === 0) return text
|
|
100
|
+
// Replacer function, not a replacement string: carried content can contain
|
|
101
|
+
// shell quoting like $'...', and a string replacement expands $' as
|
|
102
|
+
// "everything after the match" and truncates the file.
|
|
103
|
+
return text.replace(PROJECT_SLOT_RE, (whole, name) => {
|
|
104
|
+
const kept = slots.get(name)
|
|
105
|
+
if (kept === undefined || kept.trim() === '') return whole
|
|
106
|
+
return `<!-- ${name}-START -->${kept}<!-- ${name}-END -->`
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function refreshMarkerSkill(src, dest, relativeRoot) {
|
|
111
|
+
for (const entry of await fse.readdir(src, { withFileTypes: true })) {
|
|
112
|
+
const sourcePath = path.join(src, entry.name)
|
|
113
|
+
const destinationPath = path.join(dest, entry.name)
|
|
114
|
+
const relativePath = path.join(relativeRoot, entry.name)
|
|
115
|
+
if (entry.isDirectory()) {
|
|
116
|
+
await refreshMarkerSkill(sourcePath, destinationPath, relativePath)
|
|
117
|
+
continue
|
|
118
|
+
}
|
|
119
|
+
const shipped = await fse.readFile(sourcePath, 'utf-8')
|
|
120
|
+
const existing = await fse.pathExists(destinationPath)
|
|
121
|
+
? await fse.readFile(destinationPath, 'utf-8')
|
|
122
|
+
: ''
|
|
123
|
+
const merged = writeProjectSlots(shipped, readProjectSlots(existing))
|
|
124
|
+
if (merged === existing) continue
|
|
125
|
+
await fse.ensureDir(path.dirname(destinationPath))
|
|
126
|
+
await fse.writeFile(destinationPath, merged, 'utf-8')
|
|
127
|
+
success(`Refreshed skill: ${relativePath}`)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function isGeneratedSkill(dest) {
|
|
132
|
+
const skillMd = path.join(dest, 'SKILL.md')
|
|
133
|
+
if (!await fse.pathExists(skillMd)) return false
|
|
134
|
+
const content = await fse.readFile(skillMd, 'utf-8')
|
|
135
|
+
return content.includes('<!-- Last updated:')
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function syncSkillFiles(src, dest, relativeRoot, cwd, manifest) {
|
|
139
|
+
for (const entry of await fse.readdir(src, { withFileTypes: true })) {
|
|
140
|
+
const sourcePath = path.join(src, entry.name)
|
|
141
|
+
const destinationPath = path.join(dest, entry.name)
|
|
142
|
+
const relativePath = path.join(relativeRoot, entry.name)
|
|
143
|
+
if (entry.isDirectory()) {
|
|
144
|
+
await syncSkillFiles(sourcePath, destinationPath, relativePath, cwd, manifest)
|
|
145
|
+
continue
|
|
146
|
+
}
|
|
147
|
+
const exists = await fse.pathExists(destinationPath)
|
|
148
|
+
if (!exists || await canUpdateManagedFile(relativePath, cwd, manifest)) {
|
|
149
|
+
await fse.ensureDir(path.dirname(destinationPath))
|
|
150
|
+
// The managed check ignores what sits between markers, so an untouched file can still hold
|
|
151
|
+
// a filled PC-PROJECT-* slot; copying the source over it erased Numa's sign-in steps.
|
|
152
|
+
const shipped = await fse.readFile(sourcePath, 'utf-8')
|
|
153
|
+
if (exists && HAS_PROJECT_SLOT.test(shipped)) {
|
|
154
|
+
const existing = await fse.readFile(destinationPath, 'utf-8')
|
|
155
|
+
await fse.writeFile(destinationPath, writeProjectSlots(shipped, readProjectSlots(existing)), 'utf-8')
|
|
156
|
+
} else {
|
|
157
|
+
await fse.copyFile(sourcePath, destinationPath)
|
|
158
|
+
}
|
|
159
|
+
await recordManagedFile(manifest, relativePath, sourcePath)
|
|
160
|
+
success(`Updated skill: ${relativePath}`)
|
|
161
|
+
continue
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// A shipped file that declares a PC-PROJECT-* slot states in its own prose
|
|
165
|
+
// that the slot is the project's and everything else is the harness's, so a
|
|
166
|
+
// hand-edited copy still takes the update: merge rather than preserve.
|
|
167
|
+
// Without this the file is frozen at whatever version the project edited,
|
|
168
|
+
// which is how five repos ended up with stale skills.
|
|
169
|
+
const shipped = await fse.readFile(sourcePath, 'utf-8')
|
|
170
|
+
if (HAS_PROJECT_SLOT.test(shipped)) {
|
|
171
|
+
const existing = await fse.readFile(destinationPath, 'utf-8')
|
|
172
|
+
const merged = writeProjectSlots(shipped, readProjectSlots(existing))
|
|
173
|
+
if (merged !== existing) {
|
|
174
|
+
await fse.writeFile(destinationPath, merged, 'utf-8')
|
|
175
|
+
success(`Refreshed skill, project slot kept: ${relativePath}`)
|
|
176
|
+
}
|
|
177
|
+
continue
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
info(`Preserving modified skill file: ${relativePath}`)
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function recordSkillSourceFiles(src, relativeRoot, manifest) {
|
|
185
|
+
for (const entry of await fse.readdir(src, { withFileTypes: true })) {
|
|
186
|
+
const sourcePath = path.join(src, entry.name)
|
|
187
|
+
const relativePath = path.join(relativeRoot, entry.name)
|
|
188
|
+
if (entry.isDirectory()) {
|
|
189
|
+
await recordSkillSourceFiles(sourcePath, relativePath, manifest)
|
|
190
|
+
} else {
|
|
191
|
+
await recordManagedFile(manifest, relativePath, sourcePath)
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function installObSkills(backlogPlatform = 'github', repoPlatform, { forceOverwrite = false, updateMode = false } = {}) {
|
|
197
|
+
const repo = repoPlatform ?? backlogPlatform
|
|
198
|
+
const destSkillsDir = path.join(process.cwd(), '.agents', 'skills')
|
|
199
|
+
await fse.ensureDir(destSkillsDir)
|
|
200
|
+
const manifest = await readUpdateManifest()
|
|
201
|
+
|
|
202
|
+
// Build the set of skill names we ship (source dirs, after rename).
|
|
203
|
+
// Only these may be removed during forceOverwrite — project-generated
|
|
204
|
+
// skills (pc-merge-risk-assess, custom loops, etc.) must survive.
|
|
205
|
+
const contentSkills = (await fse.readdir(CONTENT_SKILLS_DIR))
|
|
206
|
+
.filter(name => !NON_INSTALLABLE_SKILLS.has(name))
|
|
207
|
+
const shippedNames = new Set()
|
|
208
|
+
for (const skill of contentSkills) {
|
|
209
|
+
const stat = await fse.stat(path.join(CONTENT_SKILLS_DIR, skill)).catch(() => null)
|
|
210
|
+
if (stat?.isDirectory()) shippedNames.add(SKILL_RENAME[skill] ?? skill)
|
|
211
|
+
}
|
|
212
|
+
// Also account for stale platform-variant names (e.g. pc-userstory-gh
|
|
213
|
+
// when the current platform is azure).
|
|
214
|
+
Object.keys(SKILL_RENAME).forEach(k => shippedNames.add(k))
|
|
215
|
+
|
|
216
|
+
if (forceOverwrite && !updateMode) {
|
|
217
|
+
for (const entry of await fse.readdir(destSkillsDir)) {
|
|
218
|
+
if (!entry.startsWith('pc-')) continue
|
|
219
|
+
if (!shippedNames.has(entry)) {
|
|
220
|
+
info(`Preserving project-generated skill: ${entry}`)
|
|
221
|
+
continue
|
|
222
|
+
}
|
|
223
|
+
// Generatable skills: preserve if already populated by /make-*
|
|
224
|
+
if (GENERATABLE_SKILLS.has(entry) && await isGeneratedSkill(path.join(destSkillsDir, entry))) {
|
|
225
|
+
info(`Preserving generated skill: ${entry}`)
|
|
226
|
+
continue
|
|
227
|
+
}
|
|
228
|
+
await fse.remove(path.join(destSkillsDir, entry))
|
|
229
|
+
info(`Removing shipped skill: ${entry}`)
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const skills = contentSkills
|
|
234
|
+
for (const skill of skills) {
|
|
235
|
+
const src = path.join(CONTENT_SKILLS_DIR, skill)
|
|
236
|
+
const destName = SKILL_RENAME[skill] ?? skill
|
|
237
|
+
const dest = path.join(destSkillsDir, destName)
|
|
238
|
+
const stat = await fse.stat(src)
|
|
239
|
+
if (!stat.isDirectory()) continue
|
|
240
|
+
if (!shouldInstallSkill(skill, backlogPlatform, repo)) {
|
|
241
|
+
if (forceOverwrite) {
|
|
242
|
+
const staleDest = path.join(destSkillsDir, skill)
|
|
243
|
+
if (skill !== destName && await fse.pathExists(staleDest)) {
|
|
244
|
+
await fse.remove(staleDest)
|
|
245
|
+
info(`Removing stale skill: ${skill}`)
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
info(`Skipping skill: ${skill} (not needed for platforms: ${backlogPlatform}/${repo})`)
|
|
249
|
+
continue
|
|
250
|
+
}
|
|
251
|
+
if (updateMode) {
|
|
252
|
+
const relativeRoot = path.join('.agents', 'skills', destName)
|
|
253
|
+
if (!await fse.pathExists(dest)) {
|
|
254
|
+
await syncSkillFiles(src, dest, relativeRoot, process.cwd(), manifest)
|
|
255
|
+
} else if (GENERATABLE_SKILLS.has(destName)) {
|
|
256
|
+
info(`Preserving generated skill: ${destName}`)
|
|
257
|
+
} else if (REFRESHABLE_MARKER_SKILLS.has(destName)) {
|
|
258
|
+
// Shipped prose refreshes; the patchers re-inject the marker pairs and
|
|
259
|
+
// any PC-PROJECT-* slot is carried over.
|
|
260
|
+
await refreshMarkerSkill(src, dest, relativeRoot)
|
|
261
|
+
} else if (MARKER_SKILLS.has(destName)) {
|
|
262
|
+
info(`Preserving project-owned skill: ${destName}`)
|
|
263
|
+
} else {
|
|
264
|
+
await syncSkillFiles(src, dest, relativeRoot, process.cwd(), manifest)
|
|
265
|
+
}
|
|
266
|
+
continue
|
|
267
|
+
}
|
|
268
|
+
if (await fse.pathExists(dest) && !forceOverwrite) {
|
|
269
|
+
info(`${destName} already exists, skipping`)
|
|
270
|
+
continue
|
|
271
|
+
}
|
|
272
|
+
// Even with forceOverwrite, never overwrite an already-generated
|
|
273
|
+
// generatable skill — its content was produced by /make-guardrails or
|
|
274
|
+
// similar, not by the shipped placeholder.
|
|
275
|
+
if (forceOverwrite && GENERATABLE_SKILLS.has(destName) && await isGeneratedSkill(dest)) {
|
|
276
|
+
info(`Preserving generated skill: ${destName}`)
|
|
277
|
+
continue
|
|
278
|
+
}
|
|
279
|
+
await fse.copy(src, dest, { overwrite: true })
|
|
280
|
+
if (!MARKER_SKILLS.has(destName) && !GENERATABLE_SKILLS.has(destName)) {
|
|
281
|
+
await recordSkillSourceFiles(src, path.join('.agents', 'skills', destName), manifest)
|
|
282
|
+
}
|
|
283
|
+
success(`${forceOverwrite ? 'Updated' : 'Installed'} skill: ${destName}`)
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
await writeUpdateManifest(manifest)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export async function installSkills(backlogPlatform = 'github', repoPlatform, opts = {}) {
|
|
290
|
+
info('Installing built-in pc-skills...')
|
|
291
|
+
await installObSkills(backlogPlatform, repoPlatform, opts)
|
|
292
|
+
console.log()
|
|
293
|
+
|
|
294
|
+
if (await fse.pathExists(CONTENT_SKILLS_LOCK)) {
|
|
295
|
+
const destLock = path.join(process.cwd(), 'skills-lock.json')
|
|
296
|
+
if (await fse.pathExists(destLock) && (opts.updateMode || !opts.forceOverwrite)) {
|
|
297
|
+
info('skills-lock.json already exists, skipping')
|
|
298
|
+
} else {
|
|
299
|
+
await fse.copy(CONTENT_SKILLS_LOCK, destLock, { overwrite: true })
|
|
300
|
+
success(`${opts.forceOverwrite ? 'Updated' : 'Installed'} skills-lock.json`)
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// npx skills experimental_install is now called once at the end of the
|
|
305
|
+
// optimization step, after the user has selected which tools to enable.
|
|
306
|
+
// This avoids multiple npx skills add calls during onboarding.
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export async function removeRetiredFiles(cwd = process.cwd()) {
|
|
310
|
+
for (const relative of RETIRED_PATHS) {
|
|
311
|
+
const target = path.join(cwd, relative)
|
|
312
|
+
if (!await fse.pathExists(target)) continue
|
|
313
|
+
await fse.remove(target)
|
|
314
|
+
info(`Removed retired: ${relative}`)
|
|
315
|
+
}
|
|
316
|
+
}
|