docks-kit 0.17.1 → 0.17.2
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/AGENTS.md +36 -32
- package/cli/docs/omp-models.md +88 -53
- package/cli/src/argv.ts +38 -469
- package/cli/src/argvSurface.ts +371 -0
- package/cli/src/argvValidate.ts +122 -0
- package/cli/src/commands/docs.ts +57 -42
- package/cli/src/commands/harnesses.ts +38 -45
- package/cli/src/commands/model.ts +48 -53
- package/cli/src/commands/models.ts +24 -26
- package/cli/src/commands/omp.ts +132 -112
- package/cli/src/commands/plugins.ts +20 -20
- package/cli/src/commands/skills.ts +22 -20
- package/cli/src/commands/status.ts +87 -80
- package/cli/src/commands/sync.ts +113 -87
- package/cli/src/commands/toolchain.ts +21 -21
- package/cli/src/commands/update.ts +139 -117
- package/cli/src/efforts.ts +35 -34
- package/cli/src/engine-native/DESIGN.md +30 -30
- package/cli/src/engine-native/bun.ts +77 -55
- package/cli/src/engine-native/claudeLsp.ts +154 -0
- package/cli/src/engine-native/claudeOptionalPlugins.ts +113 -0
- package/cli/src/engine-native/claudePluginPasses.ts +348 -0
- package/cli/src/engine-native/claudeRemovals.ts +195 -0
- package/cli/src/engine-native/claudeRetired.ts +4 -4
- package/cli/src/engine-native/claudeRuntime.ts +94 -68
- package/cli/src/engine-native/claudeSettings.ts +174 -0
- package/cli/src/engine-native/claudeSettingsModifiers.ts +50 -54
- package/cli/src/engine-native/claudeSync.ts +176 -456
- package/cli/src/engine-native/codexConfig.ts +158 -0
- package/cli/src/engine-native/codexHooks.ts +91 -0
- package/cli/src/engine-native/codexPlugins.ts +290 -0
- package/cli/src/engine-native/codexStatus.ts +24 -0
- package/cli/src/engine-native/codexSync.ts +110 -568
- package/cli/src/engine-native/codexToml.ts +140 -138
- package/cli/src/engine-native/deps.ts +144 -107
- package/cli/src/engine-native/engineCtx.ts +126 -0
- package/cli/src/engine-native/exec.ts +98 -87
- package/cli/src/engine-native/failures.ts +3 -3
- package/cli/src/engine-native/harnesses.ts +62 -72
- package/cli/src/engine-native/index.ts +34 -315
- package/cli/src/engine-native/jq.ts +25 -24
- package/cli/src/engine-native/logger.ts +98 -100
- package/cli/src/engine-native/models.ts +50 -45
- package/cli/src/engine-native/modes.ts +83 -80
- package/cli/src/engine-native/ompFileDeploy.ts +111 -0
- package/cli/src/engine-native/ompMarketplace.ts +136 -0
- package/cli/src/engine-native/ompOverlay.ts +67 -64
- package/cli/src/engine-native/ompPaths.ts +49 -38
- package/cli/src/engine-native/ompPlugins.ts +170 -0
- package/cli/src/engine-native/ompSync.ts +64 -408
- package/cli/src/engine-native/ompYaml.ts +43 -41
- package/cli/src/engine-native/os/darwin.ts +5 -5
- package/cli/src/engine-native/os/index.ts +15 -15
- package/cli/src/engine-native/os/linux.ts +5 -5
- package/cli/src/engine-native/os/posix.ts +28 -24
- package/cli/src/engine-native/os/targets.ts +17 -17
- package/cli/src/engine-native/os/types.ts +31 -31
- package/cli/src/engine-native/os/windows.ts +61 -61
- package/cli/src/engine-native/parseArgs.ts +122 -372
- package/cli/src/engine-native/parseHelp.ts +66 -0
- package/cli/src/engine-native/parseModifiers.ts +222 -0
- package/cli/src/engine-native/services.ts +34 -34
- package/cli/src/engine-native/settings.ts +15 -15
- package/cli/src/engine-native/sharedTypes.d.ts +46 -0
- package/cli/src/engine-native/skillsInstall.ts +105 -0
- package/cli/src/engine-native/skillsLinks.ts +203 -0
- package/cli/src/engine-native/skillsManifest.ts +16 -0
- package/cli/src/engine-native/skillsPrune.ts +108 -0
- package/cli/src/engine-native/skillsSync.ts +28 -362
- package/cli/src/engine-native/syncConcurrency.ts +55 -0
- package/cli/src/engine-native/syncDispatch.ts +141 -0
- package/cli/src/engine-native/toolchain.ts +65 -62
- package/cli/src/engine.ts +65 -57
- package/cli/src/generated/sotPayload.ts +7 -7
- package/cli/src/kitHome.ts +44 -40
- package/cli/src/main.ts +63 -56
- package/cli/src/manifests.ts +53 -54
- package/cli/src/md.d.ts +2 -2
- package/cli/src/payload.ts +9 -9
- package/cli/src/services.ts +21 -13
- package/cli/tsconfig.json +4 -1
- package/package.json +9 -2
- package/cli/src/engine-native/claudePlugins.ts +0 -510
|
@@ -2,384 +2,50 @@
|
|
|
2
2
|
* EngineNative `sync agents` pipeline: universal-skill bootstrap
|
|
3
3
|
* (`npx skills@<pin> add`), Claude symlink healing, --prune reconcile against
|
|
4
4
|
* the kit-managed snapshot, and the snapshot write.
|
|
5
|
+
*
|
|
6
|
+
* Split: manifest parsing lives in `skillsManifest.ts`, link mechanics and
|
|
7
|
+
* healing in `skillsLinks.ts`, the install pass in `skillsInstall.ts`, and
|
|
8
|
+
* prune plus snapshot tracking in `skillsPrune.ts`. This module keeps the
|
|
9
|
+
* orchestration and re-exports the public surface so existing import paths
|
|
10
|
+
* keep working.
|
|
5
11
|
*/
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
readlinkSync,
|
|
13
|
-
rmSync,
|
|
14
|
-
statSync,
|
|
15
|
-
symlinkSync,
|
|
16
|
-
writeFileSync
|
|
17
|
-
} from "node:fs"
|
|
18
|
-
import { dirname, relative, resolve } from "node:path"
|
|
19
|
-
import { payloadText } from "../payload"
|
|
20
|
-
import { p, spawnProcess, writeTextIfChanged } from "./exec"
|
|
21
|
-
import type { Ctx } from "./index"
|
|
22
|
-
import { compareCodepoints } from "./jq"
|
|
23
|
-
import { hostOs, type DirectoryLinkKind } from "./os"
|
|
24
|
-
import { ExitError } from "./parseArgs"
|
|
25
|
-
import type { EngineServices } from "./services"
|
|
26
|
-
import { field } from "./toolchain"
|
|
12
|
+
import { mkdirSync } from "node:fs";
|
|
13
|
+
import { payloadText } from "../payload";
|
|
14
|
+
import { p } from "./exec";
|
|
15
|
+
import type { Ctx } from "./index";
|
|
16
|
+
import { syncUniversal, type SkillsState } from "./skillsInstall";
|
|
17
|
+
import { reconcileRemovals, updateSnapshot } from "./skillsPrune";
|
|
27
18
|
|
|
28
|
-
export
|
|
29
|
-
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export type LinkOutcome = "symlink" | "junction" | "copy" | "failed"
|
|
33
|
-
|
|
34
|
-
/** Lets heal and prune distinguish a kit-owned copy from a user's real directory. */
|
|
35
|
-
export const COPY_MARKER = ".docks-kit-copied-skill"
|
|
19
|
+
export type { SkillsState } from "./skillsInstall";
|
|
20
|
+
export { COPY_MARKER, linkOrCopy, type LinkOutcome } from "./skillsLinks";
|
|
21
|
+
export { normalizeManifest } from "./skillsManifest";
|
|
36
22
|
|
|
37
23
|
export async function skillsSync(ctx: Ctx): Promise<SkillsState> {
|
|
38
|
-
const state: SkillsState = { present: 0 }
|
|
39
|
-
const skillsDir = p(ctx.agentsDir, "skills")
|
|
40
|
-
const manifest = payloadText("SoT/.agents/skills.txt")
|
|
41
|
-
const snapshot = p(ctx.agentsDir, ".kit-managed-skills")
|
|
42
|
-
|
|
43
|
-
if (!ctx.dryRun) mkdirSync(skillsDir, { recursive: true })
|
|
44
|
-
|
|
45
|
-
await syncUniversal(ctx, state, skillsDir, manifest)
|
|
46
|
-
const failedRemovals = ctx.prune ? await reconcileRemovals(ctx, manifest, snapshot) : []
|
|
47
|
-
updateSnapshot(ctx, manifest, snapshot, failedRemovals)
|
|
48
|
-
return state
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/** skills::_skills_cli — the pinned npx package spec. */
|
|
52
|
-
function skillsCli(ctx: Ctx): string {
|
|
53
|
-
const version = field("skills-cli", "verified")
|
|
54
|
-
if (version !== "") return `skills@${version}`
|
|
55
|
-
ctx.services.logger.err("Universal skills sync aborted because SoT/toolchain.json has no verified skills-cli pin")
|
|
56
|
-
throw new ExitError(1)
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/** skills::_normalize_manifest — cleaned slugs, one per line. */
|
|
60
|
-
export function normalizeManifest(content: string): Array<string> {
|
|
61
|
-
const out: Array<string> = []
|
|
62
|
-
for (const line of content.split("\n")) {
|
|
63
|
-
if (/^[ \t]*#/.test(line)) continue
|
|
64
|
-
if (/^[ \t]*$/.test(line)) continue
|
|
65
|
-
const cleaned = line.replace(/[ \t]*#.*$/, "").replace(/[ \t\r]+/g, "")
|
|
66
|
-
if (cleaned.length > 0) out.push(cleaned)
|
|
67
|
-
}
|
|
68
|
-
return out
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function readSlugs(file: string): Array<string> {
|
|
72
|
-
return existsSync(file) ? normalizeManifest(readFileSync(file, "utf8")) : []
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
async function syncUniversal(ctx: Ctx, state: SkillsState, skillsDir: string, manifest: string): Promise<void> {
|
|
76
|
-
const { change, clearProgress, echo, progress, verbose, warn } = ctx.services.logger
|
|
77
|
-
if (ctx.services.deps.probe("npx").state === "missing") {
|
|
78
|
-
ctx.services.deps.warnMissing("npx", ctx.services.logger, "skipping universal skills bootstrap")
|
|
79
|
-
return
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
let added = 0
|
|
83
|
-
let already = 0
|
|
84
|
-
let failed = 0
|
|
85
|
-
let healed = 0
|
|
86
|
-
|
|
87
|
-
for (const slug of normalizeManifest(manifest)) {
|
|
88
|
-
const base = slug.slice(slug.lastIndexOf("/") + 1)
|
|
89
|
-
|
|
90
|
-
if (ctx.dryRun) {
|
|
91
|
-
if (isDir(p(skillsDir, base))) {
|
|
92
|
-
echo(`[dry-run] universal skill present: ${base}`)
|
|
93
|
-
healClaudeSymlink(ctx, skillsDir, base)
|
|
94
|
-
} else {
|
|
95
|
-
echo(`[dry-run] npx ${skillsCli(ctx)} add ${slug} -g -y -a claude-code codex`)
|
|
96
|
-
}
|
|
97
|
-
continue
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
if (isDir(p(skillsDir, base))) {
|
|
101
|
-
already++
|
|
102
|
-
if (healClaudeSymlink(ctx, skillsDir, base)) healed++
|
|
103
|
-
continue
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
progress(`Installing universal skill ${slug}...`)
|
|
107
|
-
const res = await spawnProcess("npx", ["--yes", skillsCli(ctx), "add", slug, "-g", "-y", "-a", "claude-code", "codex"], {
|
|
108
|
-
stdio: "ignore"
|
|
109
|
-
})
|
|
110
|
-
clearProgress()
|
|
111
|
-
if (res.error === undefined && res.exitCode === 0) {
|
|
112
|
-
added++
|
|
113
|
-
} else {
|
|
114
|
-
warn(`Failed to install universal skill: ${slug}`)
|
|
115
|
-
failed++
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
if (ctx.dryRun) return
|
|
120
|
-
|
|
121
|
-
state.present = added + already
|
|
122
|
-
|
|
123
|
-
if (added > 0) {
|
|
124
|
-
change(`Universal skills synced (+${added} new, ${already} already present)`)
|
|
125
|
-
ctx.nextStepTriggers.skillsRestart = true
|
|
126
|
-
} else {
|
|
127
|
-
verbose(`Universal skills already in sync (${already} present)`)
|
|
128
|
-
}
|
|
129
|
-
if (healed > 0) {
|
|
130
|
-
change(`Claude per-tool symlinks healed (+${healed}) — canonical present, ~/.claude/skills/<name> was missing or broken`)
|
|
131
|
-
ctx.nextStepTriggers.skillsRestart = true
|
|
132
|
-
}
|
|
133
|
-
if (failed > 0) {
|
|
134
|
-
warn(`${failed} skill install(s) failed — re-run sync or install manually with: npx skills add <slug> -g -y -a claude-code codex`)
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function isDir(path: string): boolean {
|
|
139
|
-
try {
|
|
140
|
-
return statSync(path).isDirectory()
|
|
141
|
-
} catch {
|
|
142
|
-
return false
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/** skills::heal_claude_symlink — true when a heal occurred. */
|
|
147
|
-
function healClaudeSymlink(ctx: Ctx, skillsDir: string, base: string): boolean {
|
|
148
|
-
const { echo, warn } = ctx.services.logger
|
|
149
|
-
const canonical = p(skillsDir, base)
|
|
150
|
-
const claudeSkillsDir = p(ctx.home, ".claude", "skills")
|
|
151
|
-
const claudeLink = p(claudeSkillsDir, base)
|
|
152
|
-
const relTarget = relative(dirname(claudeLink), canonical)
|
|
153
|
-
|
|
154
|
-
if (!isDir(canonical)) return false
|
|
155
|
-
|
|
156
|
-
const linkStat = lstat(claudeLink)
|
|
157
|
-
if (linkStat?.isSymbolicLink() === true) {
|
|
158
|
-
const current = safeReadlink(claudeLink)
|
|
159
|
-
if (current === relTarget) return false
|
|
160
|
-
if (ctx.dryRun) {
|
|
161
|
-
echo(`[dry-run] would replace stale Claude symlink: ~/.claude/skills/${base} -> ${current} (correct: ${relTarget})`)
|
|
162
|
-
return true
|
|
163
|
-
}
|
|
164
|
-
if (!removeLink(claudeLink)) {
|
|
165
|
-
warn(`could not remove stale link ~/.claude/skills/${base} — remove it manually, then re-run sync`)
|
|
166
|
-
return false
|
|
167
|
-
}
|
|
168
|
-
} else if (linkStat !== undefined) {
|
|
169
|
-
if (!isKitOwnedCopy(claudeLink)) {
|
|
170
|
-
warn(`~/.claude/skills/${base} exists as a real path (not a symlink) — leaving alone; remove manually if it's stale`)
|
|
171
|
-
return false
|
|
172
|
-
}
|
|
173
|
-
if (ctx.dryRun) {
|
|
174
|
-
echo(`[dry-run] would replace kit-created Claude copy: ~/.claude/skills/${base} -> ${relTarget}`)
|
|
175
|
-
return true
|
|
176
|
-
}
|
|
177
|
-
if (!removeKitOwnedCopy(claudeLink)) {
|
|
178
|
-
warn(`could not remove kit-created copy ~/.claude/skills/${base} — remove it manually, then re-run sync`)
|
|
179
|
-
return false
|
|
180
|
-
}
|
|
181
|
-
} else if (ctx.dryRun) {
|
|
182
|
-
echo(`[dry-run] would create missing Claude symlink: ~/.claude/skills/${base} -> ${relTarget}`)
|
|
183
|
-
return true
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
mkdirSync(claudeSkillsDir, { recursive: true })
|
|
187
|
-
return linkOrCopyWithWarnings(relTarget, claudeLink, ctx.services) !== "failed"
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
function lstat(path: string): ReturnType<typeof lstatSync> | undefined {
|
|
191
|
-
try {
|
|
192
|
-
return lstatSync(path)
|
|
193
|
-
} catch {
|
|
194
|
-
return undefined
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
/**
|
|
199
|
-
* A link only counts when it RESOLVES to the skill directory. Windows picks a
|
|
200
|
-
* symlink's file-or-directory type by autodetecting the target against the
|
|
201
|
-
* process working directory, not the link's own directory, so a relative
|
|
202
|
-
* target can yield a symlink that exists but resolves to nothing. Checking
|
|
203
|
-
* resolution is what makes the next mechanism in the chain reachable.
|
|
204
|
-
*/
|
|
205
|
-
function linksToDirectory(path: string): boolean {
|
|
206
|
-
if (lstat(path)?.isSymbolicLink() !== true) return false
|
|
207
|
-
try {
|
|
208
|
-
return statSync(path).isDirectory()
|
|
209
|
-
} catch {
|
|
210
|
-
return false
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
function isKitOwnedCopy(path: string): boolean {
|
|
215
|
-
return lstat(path)?.isDirectory() === true && existsSync(p(path, COPY_MARKER))
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
function removeKitOwnedCopy(path: string): boolean {
|
|
219
|
-
if (!isKitOwnedCopy(path)) return false
|
|
220
|
-
try {
|
|
221
|
-
rmSync(path, { recursive: true, force: true })
|
|
222
|
-
return true
|
|
223
|
-
} catch {
|
|
224
|
-
return lstat(path) === undefined
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
function safeReadlink(path: string): string {
|
|
229
|
-
try {
|
|
230
|
-
return readlinkSync(path)
|
|
231
|
-
} catch {
|
|
232
|
-
return ""
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
/** Remove a symlink without touching a real directory. */
|
|
238
|
-
function removeLink(path: string): boolean {
|
|
239
|
-
try {
|
|
240
|
-
rmSync(path, { force: true })
|
|
241
|
-
return true
|
|
242
|
-
} catch {
|
|
243
|
-
return lstat(path) === undefined
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
/** skills::_link_or_copy — try directory links in host order, then a marked copy. */
|
|
248
|
-
export function linkOrCopy(
|
|
249
|
-
target: string,
|
|
250
|
-
link: string,
|
|
251
|
-
kinds: ReadonlyArray<DirectoryLinkKind> = hostOs().directoryLinkKinds
|
|
252
|
-
): LinkOutcome {
|
|
253
|
-
const resolvedLink = resolve(link)
|
|
254
|
-
const absoluteTarget = resolve(dirname(resolvedLink), target)
|
|
255
|
-
if (absoluteTarget === resolvedLink) return "symlink"
|
|
256
|
-
removeLink(link)
|
|
257
|
-
|
|
258
|
-
for (const kind of kinds) {
|
|
259
|
-
try {
|
|
260
|
-
if (kind === "symlink") {
|
|
261
|
-
symlinkSync(target, link)
|
|
262
|
-
} else {
|
|
263
|
-
symlinkSync(absoluteTarget, link, "junction")
|
|
264
|
-
}
|
|
265
|
-
if (linksToDirectory(link)) return kind
|
|
266
|
-
} catch {
|
|
267
|
-
// The runtime decides whether each mechanism works; try the next one.
|
|
268
|
-
}
|
|
269
|
-
removeLink(link)
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
const copyDestinationWasAbsent = lstat(link) === undefined
|
|
273
|
-
try {
|
|
274
|
-
cpSync(absoluteTarget, link, { recursive: true })
|
|
275
|
-
writeFileSync(p(link, COPY_MARKER), "")
|
|
276
|
-
return "copy"
|
|
277
|
-
} catch {
|
|
278
|
-
if (copyDestinationWasAbsent) {
|
|
279
|
-
try {
|
|
280
|
-
rmSync(link, { recursive: true, force: true })
|
|
281
|
-
} catch {
|
|
282
|
-
// The outcome remains failed; a later sync can retry the destination.
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
return "failed"
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
function linkOrCopyWithWarnings(target: string, link: string, services: EngineServices): LinkOutcome {
|
|
290
|
-
const outcome = linkOrCopy(target, link)
|
|
291
|
-
if (outcome === "copy") {
|
|
292
|
-
services.logger.warn(`created copy fallback ${link} because directory linking is unavailable — a later sync will restore a real link once linking works`)
|
|
293
|
-
} else if (outcome === "failed") {
|
|
294
|
-
services.logger.warn(`could not create symlink ${link}`)
|
|
295
|
-
}
|
|
296
|
-
return outcome
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
// ----------------------------------------------------- prune + snapshot ----
|
|
300
|
-
|
|
301
|
-
async function reconcileRemovals(ctx: Ctx, manifest: string, snapshot: string): Promise<Array<string>> {
|
|
302
|
-
const { change, clearProgress, echo, progress, warn } = ctx.services.logger
|
|
303
|
-
if (!existsSync(snapshot)) {
|
|
304
|
-
if (ctx.dryRun) {
|
|
305
|
-
echo(
|
|
306
|
-
`[dry-run] (--prune) no kit-managed-skills snapshot yet; first real sync writes ${snapshot}, then future --prune runs reconcile against it`
|
|
307
|
-
)
|
|
308
|
-
}
|
|
309
|
-
return []
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
const current = normalizeManifest(manifest)
|
|
313
|
-
const currentBases = new Set(current.map((slug) => slug.slice(slug.lastIndexOf("/") + 1)))
|
|
314
|
-
let removed = 0
|
|
315
|
-
let failed = 0
|
|
316
|
-
const failedSlugs: Array<string> = []
|
|
317
|
-
for (const slug of readSlugs(snapshot)) {
|
|
318
|
-
if (current.includes(slug)) continue
|
|
319
|
-
const base = slug.slice(slug.lastIndexOf("/") + 1)
|
|
320
|
-
if (currentBases.has(base)) continue
|
|
321
|
-
const claudeEntry = p(ctx.home, ".claude", "skills", base)
|
|
322
|
-
const managedClaudeEntry = lstat(claudeEntry)?.isSymbolicLink() === true || isKitOwnedCopy(claudeEntry)
|
|
323
|
-
if (ctx.dryRun) {
|
|
324
|
-
echo(`[dry-run] kit-managed skill no longer in SoT — would remove: ${base}`)
|
|
325
|
-
if (managedClaudeEntry) {
|
|
326
|
-
echo(`[dry-run] kit-managed Claude skill entry — would remove: ~/.claude/skills/${base}`)
|
|
327
|
-
}
|
|
328
|
-
continue
|
|
329
|
-
}
|
|
330
|
-
progress(`Removing universal skill ${base}...`)
|
|
331
|
-
const res = await spawnProcess("npx", ["--yes", skillsCli(ctx), "remove", "--global", base, "-y"], {
|
|
332
|
-
stdio: "ignore"
|
|
333
|
-
})
|
|
334
|
-
clearProgress()
|
|
335
|
-
if (res.error !== undefined || res.exitCode !== 0) {
|
|
336
|
-
warn(`Failed to remove kit-managed skill: ${base}`)
|
|
337
|
-
failed++
|
|
338
|
-
failedSlugs.push(slug)
|
|
339
|
-
continue
|
|
340
|
-
}
|
|
341
|
-
if (managedClaudeEntry) {
|
|
342
|
-
const entryStat = lstat(claudeEntry)
|
|
343
|
-
const removedClaudeEntry = entryStat === undefined
|
|
344
|
-
|| (entryStat.isSymbolicLink() ? removeLink(claudeEntry) : removeKitOwnedCopy(claudeEntry))
|
|
345
|
-
if (!removedClaudeEntry) {
|
|
346
|
-
warn(`Failed to remove kit-managed Claude skill entry: ${base}`)
|
|
347
|
-
failed++
|
|
348
|
-
failedSlugs.push(slug)
|
|
349
|
-
continue
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
removed++
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
if (removed > 0) {
|
|
356
|
-
change(`Kit-managed skills removed (-${removed})`)
|
|
357
|
-
ctx.nextStepTriggers.skillsRestart = true
|
|
358
|
-
}
|
|
359
|
-
if (failed > 0) warn(`${failed} skill remove(s) failed — re-run with --prune or run: npx skills remove --global <name> -y`)
|
|
360
|
-
return failedSlugs
|
|
361
|
-
}
|
|
24
|
+
const state: SkillsState = { present: 0 };
|
|
25
|
+
const skillsDir = p(ctx.agentsDir, "skills");
|
|
26
|
+
const manifest = payloadText("SoT/.agents/skills.txt");
|
|
27
|
+
const snapshot = p(ctx.agentsDir, ".kit-managed-skills");
|
|
362
28
|
|
|
363
|
-
|
|
364
|
-
if (ctx.dryRun) return
|
|
29
|
+
if (!ctx.dryRun) mkdirSync(skillsDir, { recursive: true });
|
|
365
30
|
|
|
366
|
-
|
|
367
|
-
const
|
|
368
|
-
|
|
31
|
+
await syncUniversal(ctx, state, skillsDir, manifest);
|
|
32
|
+
const failedRemovals = ctx.prune ? await reconcileRemovals(ctx, manifest, snapshot) : [];
|
|
33
|
+
updateSnapshot(ctx, manifest, snapshot, failedRemovals);
|
|
34
|
+
return state;
|
|
369
35
|
}
|
|
370
36
|
|
|
371
37
|
// -------------------------------------------------------------- summary ----
|
|
372
38
|
|
|
373
39
|
export function skillsSummary(ctx: Ctx, state: SkillsState): void {
|
|
374
|
-
const { echo } = ctx.services.logger
|
|
375
|
-
echo(`Skills: ${p(ctx.agentsDir, "skills")}`)
|
|
40
|
+
const { echo } = ctx.services.logger;
|
|
41
|
+
echo(`Skills: ${p(ctx.agentsDir, "skills")}`);
|
|
376
42
|
if (!ctx.dryRun) {
|
|
377
|
-
echo(` ${state.present} universal skill(s) installed`)
|
|
43
|
+
echo(` ${state.present} universal skill(s) installed`);
|
|
378
44
|
}
|
|
379
45
|
}
|
|
380
46
|
|
|
381
47
|
export function skillsNextSteps(ctx: Ctx): Array<string> {
|
|
382
48
|
return ctx.verbose || ctx.nextStepTriggers.skillsRestart
|
|
383
49
|
? ["Restart Claude Code (and Codex) to discover newly installed universal skills."]
|
|
384
|
-
: []
|
|
50
|
+
: [];
|
|
385
51
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EngineNative sync scheduling: bounded-concurrency runner plus the
|
|
3
|
+
* manifest-driven serialization rule. Split from index.ts, which re-exports
|
|
4
|
+
* the public surface so existing import paths keep working.
|
|
5
|
+
*/
|
|
6
|
+
import { normalizeManifest } from "./skillsManifest";
|
|
7
|
+
|
|
8
|
+
export type SyncConcurrency = 1 | 2 | 3;
|
|
9
|
+
export type SyncTask<T> = () => Promise<T>;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Run input-ordered tasks with bounded overlap. Once one task rejects, queued
|
|
13
|
+
* tasks stay queued while already-started tasks drain; the earliest rejection
|
|
14
|
+
* in input order is then propagated.
|
|
15
|
+
*/
|
|
16
|
+
export async function runBounded<T>(
|
|
17
|
+
tasks: ReadonlyArray<SyncTask<T>>,
|
|
18
|
+
concurrency: SyncConcurrency,
|
|
19
|
+
): Promise<Array<T>> {
|
|
20
|
+
const results = new Array<T>(tasks.length);
|
|
21
|
+
const failures = new Map<number, unknown>();
|
|
22
|
+
let nextIndex = 0;
|
|
23
|
+
let stopped = false;
|
|
24
|
+
|
|
25
|
+
const workers = Array.from({ length: Math.min(concurrency, tasks.length) }, async () => {
|
|
26
|
+
for (;;) {
|
|
27
|
+
if (stopped || nextIndex >= tasks.length) return;
|
|
28
|
+
const index = nextIndex;
|
|
29
|
+
nextIndex += 1;
|
|
30
|
+
try {
|
|
31
|
+
results[index] = await tasks[index]!();
|
|
32
|
+
} catch (error) {
|
|
33
|
+
failures.set(index, error);
|
|
34
|
+
stopped = true;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
await Promise.all(workers);
|
|
39
|
+
|
|
40
|
+
for (let index = 0; index < tasks.length; index += 1) {
|
|
41
|
+
if (failures.has(index)) throw failures.get(index);
|
|
42
|
+
}
|
|
43
|
+
return results;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function syncConcurrencyForManifest(
|
|
47
|
+
configured: SyncConcurrency,
|
|
48
|
+
manifest: string,
|
|
49
|
+
claudeSelected: boolean,
|
|
50
|
+
skillsSelected: boolean,
|
|
51
|
+
): SyncConcurrency {
|
|
52
|
+
if (!claudeSelected || !skillsSelected || normalizeManifest(manifest).length === 0)
|
|
53
|
+
return configured;
|
|
54
|
+
return 1;
|
|
55
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EngineNative pipeline dispatch: select the requested harness pipelines,
|
|
3
|
+
* run them under the concurrency cap, then print the summary and advice.
|
|
4
|
+
* Split from index.ts; the `sync`/`model`/`toolchain` entry routing still
|
|
5
|
+
* lives there.
|
|
6
|
+
*/
|
|
7
|
+
import { payloadText } from "../payload";
|
|
8
|
+
import { claudeNextSteps, claudeSummary, claudeSync, type ClaudeRuntimeState } from "./claudeSync";
|
|
9
|
+
import { codexNextSteps, codexSummary, codexSync } from "./codexSync";
|
|
10
|
+
import type { Ctx } from "./index";
|
|
11
|
+
import { ompNextSteps, ompSummary, ompSync, type OmpState } from "./ompSync";
|
|
12
|
+
import { skillsNextSteps, skillsSummary, skillsSync, type SkillsState } from "./skillsSync";
|
|
13
|
+
import { ExitError, parseArgs, validateModifierFlags } from "./parseArgs";
|
|
14
|
+
import {
|
|
15
|
+
runBounded,
|
|
16
|
+
syncConcurrencyForManifest,
|
|
17
|
+
type SyncConcurrency,
|
|
18
|
+
type SyncTask,
|
|
19
|
+
} from "./syncConcurrency";
|
|
20
|
+
|
|
21
|
+
export async function engineSync(ctx: Ctx, args: ReadonlyArray<string>): Promise<number> {
|
|
22
|
+
const { acquireTerminal, echo, err } = ctx.services.logger;
|
|
23
|
+
parseArgs(ctx, args);
|
|
24
|
+
validateModifierFlags(ctx);
|
|
25
|
+
const configuredConcurrency = process.env["DOCKS_KIT_SYNC_CONCURRENCY"];
|
|
26
|
+
if (configuredConcurrency === undefined || configuredConcurrency === "") {
|
|
27
|
+
ctx.syncConcurrency = 3;
|
|
28
|
+
} else if (
|
|
29
|
+
configuredConcurrency === "1" ||
|
|
30
|
+
configuredConcurrency === "2" ||
|
|
31
|
+
configuredConcurrency === "3"
|
|
32
|
+
) {
|
|
33
|
+
ctx.syncConcurrency = Number(configuredConcurrency) as SyncConcurrency;
|
|
34
|
+
} else {
|
|
35
|
+
err("DOCKS_KIT_SYNC_CONCURRENCY must be 1, 2, or 3");
|
|
36
|
+
throw new ExitError(2);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
type PipelineResult =
|
|
40
|
+
| { readonly kind: "claude"; readonly runtime: ClaudeRuntimeState }
|
|
41
|
+
| { readonly kind: "codex" }
|
|
42
|
+
| { readonly kind: "skills"; readonly state: SkillsState }
|
|
43
|
+
| { readonly kind: "omp"; readonly state: OmpState };
|
|
44
|
+
interface SelectedPipeline {
|
|
45
|
+
readonly name: string;
|
|
46
|
+
readonly run: SyncTask<PipelineResult>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const selected: Array<SelectedPipeline> = [];
|
|
50
|
+
if (ctx.syncClaude) {
|
|
51
|
+
selected.push({
|
|
52
|
+
name: "Claude",
|
|
53
|
+
run: async () => ({ kind: "claude", runtime: await claudeSync(ctx) }),
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
if (ctx.syncCodex) {
|
|
57
|
+
selected.push({
|
|
58
|
+
name: "Codex",
|
|
59
|
+
run: async () => {
|
|
60
|
+
await codexSync(ctx);
|
|
61
|
+
return { kind: "codex" };
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
if (ctx.syncAgents) {
|
|
66
|
+
selected.push({
|
|
67
|
+
name: "skills",
|
|
68
|
+
run: async () => ({ kind: "skills", state: await skillsSync(ctx) }),
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
if (ctx.syncOmp) {
|
|
72
|
+
selected.push({
|
|
73
|
+
name: "omp",
|
|
74
|
+
run: async () => ({ kind: "omp", state: await ompSync(ctx) }),
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// A populated skills manifest deploys with `-a claude-code codex`, and symlink healing also writes into Claude's tree.
|
|
79
|
+
ctx.syncConcurrency = syncConcurrencyForManifest(
|
|
80
|
+
ctx.syncConcurrency,
|
|
81
|
+
payloadText("SoT/.agents/skills.txt"),
|
|
82
|
+
ctx.syncClaude,
|
|
83
|
+
ctx.syncAgents,
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
const remaining = new Set(selected.map(({ name }) => name));
|
|
87
|
+
const lease = acquireTerminal(`Syncing ${[...remaining].join(", ")}...`);
|
|
88
|
+
ctx.terminalLease = lease;
|
|
89
|
+
let results: Array<PipelineResult>;
|
|
90
|
+
try {
|
|
91
|
+
const tasks = selected.map(({ name, run }): SyncTask<PipelineResult> => async () => {
|
|
92
|
+
try {
|
|
93
|
+
return await run();
|
|
94
|
+
} finally {
|
|
95
|
+
remaining.delete(name);
|
|
96
|
+
if (remaining.size > 0) lease.update(`Syncing ${[...remaining].join(", ")}...`);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
results = await runBounded(tasks, ctx.syncConcurrency);
|
|
100
|
+
} finally {
|
|
101
|
+
lease.release();
|
|
102
|
+
ctx.terminalLease = undefined;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const claudeRan = ctx.syncClaude;
|
|
106
|
+
const codexRan = ctx.syncCodex;
|
|
107
|
+
let claudeRuntime: ClaudeRuntimeState | undefined;
|
|
108
|
+
let skillsState: SkillsState | undefined;
|
|
109
|
+
let ompState: OmpState | undefined;
|
|
110
|
+
for (const result of results) {
|
|
111
|
+
if (result.kind === "claude") claudeRuntime = result.runtime;
|
|
112
|
+
else if (result.kind === "skills") skillsState = result.state;
|
|
113
|
+
else if (result.kind === "omp") ompState = result.state;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
echo("");
|
|
117
|
+
echo("--- Sync complete ---");
|
|
118
|
+
echo(`Repo: ${ctx.repoDir}`);
|
|
119
|
+
if (claudeRuntime !== undefined) claudeSummary(ctx, claudeRuntime);
|
|
120
|
+
if (codexRan) codexSummary(ctx);
|
|
121
|
+
if (skillsState !== undefined) skillsSummary(ctx, skillsState);
|
|
122
|
+
if (ompState !== undefined) ompSummary(ctx, ompState);
|
|
123
|
+
|
|
124
|
+
const advice = [
|
|
125
|
+
...(claudeRan ? claudeNextSteps(ctx) : []),
|
|
126
|
+
...(codexRan ? codexNextSteps(ctx) : []),
|
|
127
|
+
...(skillsState !== undefined ? skillsNextSteps(ctx) : []),
|
|
128
|
+
...(ompState !== undefined ? ompNextSteps(ctx) : []),
|
|
129
|
+
];
|
|
130
|
+
if (advice.length > 0) {
|
|
131
|
+
echo("");
|
|
132
|
+
for (const line of advice) echo(line);
|
|
133
|
+
}
|
|
134
|
+
if (ctx.failures.length > 0) {
|
|
135
|
+
echo("");
|
|
136
|
+
echo("--- Failures ---");
|
|
137
|
+
for (const failure of ctx.failures) echo(`- ${failure}`);
|
|
138
|
+
return 1;
|
|
139
|
+
}
|
|
140
|
+
return 0;
|
|
141
|
+
}
|