docks-kit 0.1.5 → 0.2.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/README.md +1 -1
- package/SoT/.codex/config.toml +1 -1
- package/SoT/models.json +5 -2
- package/cli/docs/flags.md +1 -0
- package/cli/docs/overview.md +3 -0
- package/cli/docs/platforms.md +6 -5
- package/cli/src/commands/model.ts +7 -3
- package/cli/src/commands/sync.ts +14 -3
- package/cli/src/commands/toolchain.ts +7 -3
- package/cli/src/engine-native/DESIGN.md +75 -2
- package/cli/src/engine-native/claudeModel.ts +9 -3
- package/cli/src/engine-native/claudeSync.ts +203 -97
- package/cli/src/engine-native/codexSync.ts +95 -47
- package/cli/src/engine-native/codexToml.ts +12 -5
- package/cli/src/engine-native/deps.ts +325 -0
- package/cli/src/engine-native/exec.ts +50 -2
- package/cli/src/engine-native/index.ts +45 -9
- package/cli/src/engine-native/logger.ts +35 -0
- package/cli/src/engine-native/models.ts +13 -12
- package/cli/src/engine-native/modes.ts +17 -10
- package/cli/src/engine-native/os.ts +29 -0
- package/cli/src/engine-native/parseArgs.ts +19 -17
- package/cli/src/engine-native/services.ts +96 -0
- package/cli/src/engine-native/skillsSync.ts +70 -51
- package/cli/src/engine-native/toolchain.ts +45 -63
- package/cli/src/engine.ts +11 -2
- package/cli/src/main.ts +3 -2
- package/cli/src/services.ts +34 -0
- package/package.json +1 -1
- package/cli/src/engine-native/output.ts +0 -20
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Platform capability seam (Output Policy in DESIGN.md) — the only engine
|
|
3
|
+
* module that reads process.platform besides exec.ts's PATH/executability
|
|
4
|
+
* primitives. Per-tool package identifiers stay in deps.ts; symlink handling
|
|
5
|
+
* stays try-then-fallback at the call site (capability-driven, not predicted).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export type PlatformName = "linux" | "darwin" | "windows" | "unknown"
|
|
9
|
+
|
|
10
|
+
export function rawPlatform(): NodeJS.Platform {
|
|
11
|
+
return process.platform
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function platformName(pf: NodeJS.Platform = rawPlatform()): PlatformName {
|
|
15
|
+
return pf === "linux" ? "linux" : pf === "darwin" ? "darwin" : pf === "win32" ? "windows" : "unknown"
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function isWindows(): boolean {
|
|
19
|
+
return rawPlatform() === "win32"
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function isLinux(): boolean {
|
|
23
|
+
return rawPlatform() === "linux"
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Shell-rc exports (bashrc/zshrc) apply only off Windows. */
|
|
27
|
+
export function shellRcApplicable(): boolean {
|
|
28
|
+
return !isWindows()
|
|
29
|
+
}
|
|
@@ -4,10 +4,8 @@
|
|
|
4
4
|
* early parser exit and is caught once in runEngineNative.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { commandExists } from "./exec"
|
|
8
7
|
import type { Ctx } from "./index"
|
|
9
8
|
import { printModels, validateClaudeModel, validateCodexModel } from "./models"
|
|
10
|
-
import { echo, err, warn } from "./output"
|
|
11
9
|
|
|
12
10
|
export class ExitError extends Error {
|
|
13
11
|
constructor(readonly code: number) {
|
|
@@ -18,6 +16,7 @@ export class ExitError extends Error {
|
|
|
18
16
|
const KNOWN_CLAUDE_OPTIN_PLUGINS = ["supabase", "n8n"]
|
|
19
17
|
|
|
20
18
|
function usage(ctx: Ctx): void {
|
|
19
|
+
const { echo } = ctx.services.logger
|
|
21
20
|
const argv0 = "docks-kit sync"
|
|
22
21
|
echo(`Usage: ${argv0} [claude] [codex] [agents] [flags]`)
|
|
23
22
|
echo("")
|
|
@@ -36,6 +35,7 @@ function usage(ctx: Ctx): void {
|
|
|
36
35
|
)
|
|
37
36
|
echo(" --skip-rtk skip optional tool bootstrap (RTK, bubblewrap)")
|
|
38
37
|
echo(" --yes auto-accept toolchain prompts (containers/CI)")
|
|
38
|
+
echo(" --verbose also print no-op confirmations (already in sync, up to date, left as-is)")
|
|
39
39
|
echo("")
|
|
40
40
|
echo("Deploy-time modifiers (deployed config only; SoT untouched; a later flag-less sync reverts)")
|
|
41
41
|
echo(
|
|
@@ -66,6 +66,7 @@ export function parseCompactWindow(v: string): string | undefined {
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
function addClaudePlugin(ctx: Ctx, name: string): void {
|
|
69
|
+
const { err } = ctx.services.logger
|
|
69
70
|
if (!KNOWN_CLAUDE_OPTIN_PLUGINS.includes(name)) {
|
|
70
71
|
err(`Unknown opt-in plugin '${name}'. Known: ${KNOWN_CLAUDE_OPTIN_PLUGINS.join(", ")}`)
|
|
71
72
|
throw new ExitError(2)
|
|
@@ -81,6 +82,7 @@ function selectTarget(ctx: Ctx, target: string): void {
|
|
|
81
82
|
}
|
|
82
83
|
|
|
83
84
|
export function parseArgs(ctx: Ctx, args: ReadonlyArray<string>): void {
|
|
85
|
+
const { err } = ctx.services.logger
|
|
84
86
|
for (const arg of args) {
|
|
85
87
|
switch (arg) {
|
|
86
88
|
case "claude":
|
|
@@ -103,12 +105,15 @@ export function parseArgs(ctx: Ctx, args: ReadonlyArray<string>): void {
|
|
|
103
105
|
case "--yes":
|
|
104
106
|
ctx.assumeYes = true
|
|
105
107
|
continue
|
|
108
|
+
case "--verbose":
|
|
109
|
+
ctx.verbose = true
|
|
110
|
+
continue
|
|
106
111
|
case "--claude-model":
|
|
107
|
-
printModels(ctx
|
|
112
|
+
printModels(ctx, "claude")
|
|
108
113
|
err("--claude-model requires a value: --claude-model=<model>")
|
|
109
114
|
throw new ExitError(2)
|
|
110
115
|
case "--codex-model":
|
|
111
|
-
printModels(ctx
|
|
116
|
+
printModels(ctx, "codex")
|
|
112
117
|
err("--codex-model requires a value: --codex-model=<model>")
|
|
113
118
|
throw new ExitError(2)
|
|
114
119
|
case "--claude-compact-window":
|
|
@@ -180,32 +185,29 @@ export function parseArgs(ctx: Ctx, args: ReadonlyArray<string>): void {
|
|
|
180
185
|
}
|
|
181
186
|
|
|
182
187
|
export function preflight(ctx: Ctx): void {
|
|
188
|
+
const { err } = ctx.services.logger
|
|
183
189
|
if (ctx.syncClaude || ctx.syncCodex) {
|
|
184
|
-
if (
|
|
185
|
-
|
|
186
|
-
? "winget install jqlang.jq (then open a new terminal)"
|
|
187
|
-
: process.platform === "darwin"
|
|
188
|
-
? "brew install jq"
|
|
189
|
-
: "sudo apt install -y jq"
|
|
190
|
-
err(`jq is required (deployed statusline/hooks call it). Install: ${hint}`)
|
|
190
|
+
if (ctx.services.deps.probe("jq").state === "missing") {
|
|
191
|
+
err(`jq is required (deployed statusline/hooks call it). Install: ${ctx.services.deps.spec("jq").installHint()}`)
|
|
191
192
|
throw new ExitError(1)
|
|
192
193
|
}
|
|
193
194
|
}
|
|
194
195
|
if (ctx.syncClaude) {
|
|
195
|
-
if (
|
|
196
|
-
err(
|
|
196
|
+
if (ctx.services.deps.probe("curl").state === "missing") {
|
|
197
|
+
err(`curl is required. Install: ${ctx.services.deps.spec("curl").installHint()}`)
|
|
197
198
|
throw new ExitError(1)
|
|
198
199
|
}
|
|
199
200
|
}
|
|
200
201
|
}
|
|
201
202
|
|
|
202
203
|
export function validateModelFlags(ctx: Ctx): void {
|
|
204
|
+
const { err, warn } = ctx.services.logger
|
|
203
205
|
if (ctx.claudeModel !== "") {
|
|
204
206
|
if (!ctx.syncClaude) {
|
|
205
207
|
warn("--claude-model ignored: claude target not selected")
|
|
206
208
|
ctx.claudeModel = ""
|
|
207
|
-
} else if (!validateClaudeModel(ctx
|
|
208
|
-
printModels(ctx
|
|
209
|
+
} else if (!validateClaudeModel(ctx, ctx.claudeModel)) {
|
|
210
|
+
printModels(ctx, "claude")
|
|
209
211
|
err(`Invalid Claude model '${ctx.claudeModel}' — use an alias above or a full claude-* ID`)
|
|
210
212
|
throw new ExitError(2)
|
|
211
213
|
}
|
|
@@ -214,8 +216,8 @@ export function validateModelFlags(ctx: Ctx): void {
|
|
|
214
216
|
if (!ctx.syncCodex) {
|
|
215
217
|
warn("--codex-model ignored: codex target not selected")
|
|
216
218
|
ctx.codexModel = ""
|
|
217
|
-
} else if (!validateCodexModel(ctx
|
|
218
|
-
printModels(ctx
|
|
219
|
+
} else if (!validateCodexModel(ctx, ctx.codexModel)) {
|
|
220
|
+
printModels(ctx, "codex")
|
|
219
221
|
err(`Invalid Codex model '${ctx.codexModel}' — must match ^[A-Za-z0-9._-]+$`)
|
|
220
222
|
throw new ExitError(2)
|
|
221
223
|
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared service factory — the single construction point for the engine's
|
|
3
|
+
* injectable capabilities (Output Policy in DESIGN.md). The Effect rim wraps
|
|
4
|
+
* these in Layers (cli/src/services.ts); the harness-private native-raw entry
|
|
5
|
+
* and engineCapture's parent-side warn call it directly, so every execution
|
|
6
|
+
* path shares one implementation.
|
|
7
|
+
*/
|
|
8
|
+
import {
|
|
9
|
+
DEPENDENCIES,
|
|
10
|
+
defaultProbeExecutor,
|
|
11
|
+
resolveDependency,
|
|
12
|
+
resolveLocation,
|
|
13
|
+
resolvePath,
|
|
14
|
+
resolveVersion,
|
|
15
|
+
type DependencySpec,
|
|
16
|
+
type DependencyLocation,
|
|
17
|
+
type ProbeExecutor,
|
|
18
|
+
type ProbeResult,
|
|
19
|
+
type ToolId
|
|
20
|
+
} from "./deps"
|
|
21
|
+
import { makeLogger, type Logger, type LoggerSinks } from "./logger"
|
|
22
|
+
import { platformName, rawPlatform, type PlatformName } from "./os"
|
|
23
|
+
|
|
24
|
+
export type { Logger } from "./logger"
|
|
25
|
+
|
|
26
|
+
export interface DependencyManager {
|
|
27
|
+
readonly spec: (id: ToolId) => DependencySpec
|
|
28
|
+
readonly probe: (id: ToolId) => ProbeResult
|
|
29
|
+
readonly version: (id: ToolId) => string
|
|
30
|
+
readonly path: (id: ToolId) => string
|
|
31
|
+
readonly location: (id: ToolId) => DependencyLocation
|
|
32
|
+
readonly latest: (id: ToolId) => string
|
|
33
|
+
readonly warnMissing: (id: ToolId, logger: Logger, context?: string) => void
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface Platform {
|
|
37
|
+
readonly raw: () => NodeJS.Platform
|
|
38
|
+
readonly name: () => PlatformName
|
|
39
|
+
readonly isWindows: () => boolean
|
|
40
|
+
readonly isLinux: () => boolean
|
|
41
|
+
readonly shellRcApplicable: () => boolean
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface EngineServices {
|
|
45
|
+
readonly logger: Logger
|
|
46
|
+
readonly deps: DependencyManager
|
|
47
|
+
readonly platform: Platform
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface EngineServiceOptions {
|
|
51
|
+
readonly sinks?: LoggerSinks
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Platform view over an injectable platform id (tests pass e.g. "win32"). */
|
|
55
|
+
export const makePlatform = (pf: NodeJS.Platform = rawPlatform()): Platform => ({
|
|
56
|
+
raw: () => pf,
|
|
57
|
+
name: () => platformName(pf),
|
|
58
|
+
isWindows: () => pf === "win32",
|
|
59
|
+
isLinux: () => pf === "linux",
|
|
60
|
+
shellRcApplicable: () => pf !== "win32"
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
/** DependencyManager whose hints default to the INJECTED platform, not the host. */
|
|
64
|
+
export const makeDependencyManager = (
|
|
65
|
+
platform: Platform,
|
|
66
|
+
exec: ProbeExecutor = defaultProbeExecutor
|
|
67
|
+
): DependencyManager => {
|
|
68
|
+
const warned = new Set<ToolId>()
|
|
69
|
+
return {
|
|
70
|
+
spec: (id) => {
|
|
71
|
+
const s = DEPENDENCIES[id]
|
|
72
|
+
return { ...s, installHint: (pf = platform.raw()) => s.installHint(pf) }
|
|
73
|
+
},
|
|
74
|
+
probe: (id) => resolveDependency(DEPENDENCIES[id], exec, platform.raw()),
|
|
75
|
+
version: (id) => resolveVersion(DEPENDENCIES[id], exec),
|
|
76
|
+
path: (id) => resolvePath(DEPENDENCIES[id], exec, platform.raw()),
|
|
77
|
+
location: (id) => resolveLocation(DEPENDENCIES[id], exec, platform.raw()),
|
|
78
|
+
latest: (id) => DEPENDENCIES[id].latest?.(exec) ?? "",
|
|
79
|
+
warnMissing: (id, logger, context) => {
|
|
80
|
+
if (warned.has(id)) return
|
|
81
|
+
warned.add(id)
|
|
82
|
+
const suffix = context !== undefined && context !== "" ? ` (${context})` : ""
|
|
83
|
+
logger.warn(`${id} not installed — ${DEPENDENCIES[id].installHint(platform.raw())}${suffix}`)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export const makeEngineServices = (opts?: EngineServiceOptions): EngineServices => {
|
|
89
|
+
const platform = makePlatform()
|
|
90
|
+
const logger = makeLogger(opts?.sinks ?? {})
|
|
91
|
+
return {
|
|
92
|
+
logger,
|
|
93
|
+
deps: makeDependencyManager(platform),
|
|
94
|
+
platform
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -7,10 +7,10 @@
|
|
|
7
7
|
import { spawnSync } from "node:child_process"
|
|
8
8
|
import { cpSync, existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync, realpathSync, rmdirSync, rmSync, statSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs"
|
|
9
9
|
import { tmpdir } from "node:os"
|
|
10
|
-
import {
|
|
10
|
+
import { p, writeFileIfChanged } from "./exec"
|
|
11
11
|
import type { Ctx } from "./index"
|
|
12
12
|
import { compareCodepoints } from "./jq"
|
|
13
|
-
import {
|
|
13
|
+
import type { EngineServices, Platform } from "./services"
|
|
14
14
|
import { ensure, field } from "./toolchain"
|
|
15
15
|
|
|
16
16
|
export interface SkillsState {
|
|
@@ -58,8 +58,9 @@ function readSlugs(file: string): Array<string> {
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
function syncUniversal(ctx: Ctx, state: SkillsState, skillsDir: string, manifest: string): void {
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
const { change, echo, verbose, warn } = ctx.services.logger
|
|
62
|
+
if (ctx.services.deps.probe("npx").state === "missing") {
|
|
63
|
+
ctx.services.deps.warnMissing("npx", ctx.services.logger, "skipping universal skills bootstrap")
|
|
63
64
|
return
|
|
64
65
|
}
|
|
65
66
|
|
|
@@ -103,12 +104,14 @@ function syncUniversal(ctx: Ctx, state: SkillsState, skillsDir: string, manifest
|
|
|
103
104
|
state.present = added + already
|
|
104
105
|
|
|
105
106
|
if (added > 0) {
|
|
106
|
-
|
|
107
|
+
change(`Universal skills synced (+${added} new, ${already} already present)`)
|
|
108
|
+
ctx.nextStepTriggers.skillsRestart = true
|
|
107
109
|
} else {
|
|
108
|
-
|
|
110
|
+
verbose(`Universal skills already in sync (${already} present)`)
|
|
109
111
|
}
|
|
110
112
|
if (healed > 0) {
|
|
111
|
-
|
|
113
|
+
change(`Claude per-tool symlinks healed (+${healed}) — canonical present, ~/.claude/skills/<name> was missing or broken`)
|
|
114
|
+
ctx.nextStepTriggers.skillsRestart = true
|
|
112
115
|
}
|
|
113
116
|
if (failed > 0) {
|
|
114
117
|
warn(`${failed} skill install(s) failed — re-run sync or install manually with: npx skills add <slug> -g -y -a claude-code codex`)
|
|
@@ -125,6 +128,7 @@ function isDir(path: string): boolean {
|
|
|
125
128
|
|
|
126
129
|
/** skills::heal_claude_symlink — true when a heal occurred. */
|
|
127
130
|
function healClaudeSymlink(ctx: Ctx, skillsDir: string, base: string): boolean {
|
|
131
|
+
const { echo, warn } = ctx.services.logger
|
|
128
132
|
const canonical = p(skillsDir, base)
|
|
129
133
|
const claudeSkillsDir = p(ctx.home, ".claude", "skills")
|
|
130
134
|
const claudeLink = p(claudeSkillsDir, base)
|
|
@@ -138,7 +142,7 @@ function healClaudeSymlink(ctx: Ctx, skillsDir: string, base: string): boolean {
|
|
|
138
142
|
if (current === relTarget) return false
|
|
139
143
|
// win32: `npx skills add` creates absolute symlinks/junctions — any link
|
|
140
144
|
// that RESOLVES to the canonical dir is healthy, not stale.
|
|
141
|
-
if (
|
|
145
|
+
if (ctx.services.platform.isWindows() && realpathEquals(claudeLink, canonical)) return false
|
|
142
146
|
if (ctx.dryRun) {
|
|
143
147
|
echo(`[dry-run] would replace stale Claude symlink: ~/.claude/skills/${base} -> ${current} (correct: ${relTarget})`)
|
|
144
148
|
return true
|
|
@@ -156,7 +160,7 @@ function healClaudeSymlink(ctx: Ctx, skillsDir: string, base: string): boolean {
|
|
|
156
160
|
}
|
|
157
161
|
|
|
158
162
|
mkdirSync(claudeSkillsDir, { recursive: true })
|
|
159
|
-
return
|
|
163
|
+
return linkOrCopyWithWarnings(relTarget, claudeLink, ctx.services)
|
|
160
164
|
}
|
|
161
165
|
|
|
162
166
|
function lstat(path: string): ReturnType<typeof lstatSync> | undefined {
|
|
@@ -201,10 +205,10 @@ function removeLink(path: string): boolean {
|
|
|
201
205
|
}
|
|
202
206
|
|
|
203
207
|
/** skills::_link_or_copy — real symlink preferred, copy fallback (Windows). */
|
|
204
|
-
export function linkOrCopy(target: string, link: string): boolean {
|
|
208
|
+
export function linkOrCopy(target: string, link: string, platform: Platform): boolean {
|
|
205
209
|
removeLink(link)
|
|
206
210
|
try {
|
|
207
|
-
symlinkSync(target, link,
|
|
211
|
+
symlinkSync(target, link, platform.isWindows() ? "dir" : undefined)
|
|
208
212
|
} catch {
|
|
209
213
|
// fall through to the copy fallback below
|
|
210
214
|
}
|
|
@@ -216,47 +220,56 @@ export function linkOrCopy(target: string, link: string): boolean {
|
|
|
216
220
|
} catch {
|
|
217
221
|
// fall through to the existence check below
|
|
218
222
|
}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
223
|
+
return existsSync(link)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function linkOrCopyWithWarnings(target: string, link: string, services: EngineServices): boolean {
|
|
227
|
+
const linked = linkOrCopy(target, link, services.platform)
|
|
228
|
+
if (!linked) {
|
|
229
|
+
services.logger.warn(`could not create ${link} (symlink and copy both failed)`)
|
|
230
|
+
} else if (lstat(link)?.isSymbolicLink() !== true) {
|
|
231
|
+
services.logger.warn(`symlinks unsupported here — ${link} is a copy (refreshed on sync; enable Windows Developer Mode for real links)`)
|
|
222
232
|
}
|
|
223
|
-
|
|
224
|
-
return false
|
|
233
|
+
return linked
|
|
225
234
|
}
|
|
226
235
|
|
|
227
236
|
// ------------------------------------------------- toolchain callbacks ----
|
|
228
237
|
|
|
229
238
|
/** skills::_agent_browser_install. */
|
|
230
|
-
export function agentBrowserInstall(mode: "install" | "upgrade", version: string): number {
|
|
239
|
+
export function agentBrowserInstall(mode: "install" | "upgrade", version: string, services: EngineServices): number {
|
|
240
|
+
const { change, verbose, warn } = services.logger
|
|
231
241
|
const verb = mode === "upgrade" ? "Upgrading" : "Installing"
|
|
232
242
|
const pkg = version !== "" ? `agent-browser@${version}` : "agent-browser"
|
|
233
|
-
const installFlags =
|
|
243
|
+
const installFlags = services.platform.isLinux() ? ["--with-deps"] : []
|
|
234
244
|
|
|
235
|
-
|
|
245
|
+
verbose(`${verb} agent-browser CLI via npm${version !== "" ? ` (pinned ${version})` : ""}...`)
|
|
236
246
|
if (spawnSync("npm", ["install", "-g", pkg], { stdio: "ignore" }).status !== 0) {
|
|
237
247
|
warn(`npm install -g ${pkg} failed. Try manually: npm install -g ${pkg}`)
|
|
238
248
|
return 1
|
|
239
249
|
}
|
|
240
250
|
|
|
241
251
|
if (mode === "install") {
|
|
242
|
-
|
|
252
|
+
warn("Downloading Chrome for Testing (~175 MB; sudo may be requested for system libs on Linux)...")
|
|
243
253
|
if (spawnSync("agent-browser", ["install", ...installFlags], { stdio: "inherit" }).status !== 0) {
|
|
244
254
|
warn(`agent-browser install failed. Re-run manually: agent-browser install ${installFlags.join(" ")}`)
|
|
245
255
|
return 1
|
|
246
256
|
}
|
|
247
257
|
}
|
|
248
|
-
const out =
|
|
258
|
+
const out = services.deps.version("agent-browser")
|
|
249
259
|
const fields = (out.split("\n")[0] ?? "").trim().split(/[ \t]+/)
|
|
250
260
|
const version2 = out !== "" ? fields[fields.length - 1] ?? "version unknown" : "version unknown"
|
|
251
|
-
|
|
261
|
+
change(`agent-browser CLI ready (${version2})`)
|
|
252
262
|
return 0
|
|
253
263
|
}
|
|
254
264
|
|
|
255
265
|
function syncAgentBrowserCli(ctx: Ctx, manifest: string): void {
|
|
266
|
+
const { warn } = ctx.services.logger
|
|
256
267
|
if (!existsSync(manifest) || !readFileSync(manifest, "utf8").split("\n").includes("vercel-labs/agent-browser")) return
|
|
257
268
|
|
|
258
|
-
if (
|
|
259
|
-
if (!ctx.dryRun)
|
|
269
|
+
if (ctx.services.deps.probe("npm").state === "missing") {
|
|
270
|
+
if (!ctx.dryRun) {
|
|
271
|
+
ctx.services.deps.warnMissing("npm", ctx.services.logger, "cannot auto-install agent-browser CLI; re-run sync after installing")
|
|
272
|
+
}
|
|
260
273
|
return
|
|
261
274
|
}
|
|
262
275
|
|
|
@@ -267,21 +280,16 @@ function syncAgentBrowserCli(ctx: Ctx, manifest: string): void {
|
|
|
267
280
|
|
|
268
281
|
/** skills::_find_bun — resolved bun path or "". */
|
|
269
282
|
function findBun(ctx: Ctx): string {
|
|
270
|
-
|
|
271
|
-
if (onPath !== "") return onPath
|
|
272
|
-
const bunInstall = process.env["BUN_INSTALL"] !== undefined && process.env["BUN_INSTALL"] !== "" ? process.env["BUN_INSTALL"] : p(ctx.home, ".bun")
|
|
273
|
-
for (const cand of [p(bunInstall, "bin", "bun"), p(ctx.home, ".bun", "bin", "bun")]) {
|
|
274
|
-
if (isExecutable(cand)) return cand
|
|
275
|
-
}
|
|
276
|
-
return ""
|
|
283
|
+
return ctx.services.deps.path("bun")
|
|
277
284
|
}
|
|
278
285
|
|
|
279
286
|
/** skills::_bun_bootstrap — bun path or "" after a failed bootstrap. */
|
|
280
|
-
export function bunBootstrap(ctx: Ctx): string {
|
|
287
|
+
export function bunBootstrap(ctx: Ctx, services: EngineServices): string {
|
|
288
|
+
const { change, warn } = services.logger
|
|
281
289
|
let bun = findBun(ctx)
|
|
282
290
|
if (bun !== "") return bun
|
|
283
291
|
|
|
284
|
-
if (
|
|
292
|
+
if (services.deps.probe("curl").state === "missing") {
|
|
285
293
|
warn("Bun and curl both missing — cannot bootstrap Bun. Install Bun manually, then re-run sync.")
|
|
286
294
|
return ""
|
|
287
295
|
}
|
|
@@ -298,41 +306,44 @@ export function bunBootstrap(ctx: Ctx): string {
|
|
|
298
306
|
warn("Bun install failed. Install manually: curl -fsSL https://bun.sh/install -o /tmp/bun.sh && bash /tmp/bun.sh")
|
|
299
307
|
return ""
|
|
300
308
|
}
|
|
301
|
-
const
|
|
302
|
-
|
|
309
|
+
const version = services.deps.version("bun")
|
|
310
|
+
change(`Bun installed (${version !== "" ? version : "version unknown"})`)
|
|
303
311
|
return bun
|
|
304
312
|
}
|
|
305
313
|
|
|
306
314
|
/** skills::_effect_solutions_install. */
|
|
307
|
-
export function effectSolutionsInstall(
|
|
308
|
-
|
|
315
|
+
export function effectSolutionsInstall(
|
|
316
|
+
ctx: Ctx
|
|
317
|
+
): (mode: "install" | "upgrade", version: string, services: EngineServices) => number {
|
|
318
|
+
return (mode, version, services) => {
|
|
319
|
+
const { change, verbose, warn } = services.logger
|
|
309
320
|
const verb = mode === "upgrade" ? "Upgrading" : "Installing"
|
|
310
321
|
const pkg = `effect-solutions@${version !== "" ? version : "latest"}`
|
|
311
322
|
|
|
312
|
-
const bun = bunBootstrap(ctx)
|
|
323
|
+
const bun = bunBootstrap(ctx, services)
|
|
313
324
|
if (bun === "") return 1
|
|
314
325
|
|
|
315
|
-
|
|
326
|
+
verbose(`${verb} effect-solutions CLI via bun${version !== "" ? ` (pinned ${version})` : ""}...`)
|
|
316
327
|
if (spawnSync(bun, ["add", "-g", pkg], { stdio: "ignore" }).status !== 0) {
|
|
317
328
|
warn(`bun add -g ${pkg} failed. Try manually: bun add -g ${pkg}`)
|
|
318
329
|
return 1
|
|
319
330
|
}
|
|
320
331
|
|
|
321
|
-
const
|
|
332
|
+
const location = services.deps.location("effect-solutions")
|
|
333
|
+
const gbin = location.binDir
|
|
322
334
|
// win32: bun writes an .exe shim (not the bare Unix name), and the
|
|
323
335
|
// ~/.local/bin link step below is Unix-only plumbing (non-interactive
|
|
324
336
|
// agent PATH) — bun's global bin is already the Windows PATH entry.
|
|
325
|
-
if (
|
|
326
|
-
|
|
327
|
-
if (found) log(`effect-solutions CLI ready (${gbin})`)
|
|
337
|
+
if (services.platform.isWindows()) {
|
|
338
|
+
if (location.path !== "") change(`effect-solutions CLI ready (${gbin})`)
|
|
328
339
|
else warn(`effect-solutions installed but no shim found under '${gbin !== "" ? gbin : "<unknown>"}' — check bun pm -g bin`)
|
|
329
340
|
return 0
|
|
330
341
|
}
|
|
331
|
-
if (
|
|
342
|
+
if (location.path !== "") {
|
|
332
343
|
mkdirSync(p(ctx.home, ".local", "bin"), { recursive: true })
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
344
|
+
linkOrCopyWithWarnings(bun, p(ctx.home, ".local", "bin", "bun"), services)
|
|
345
|
+
linkOrCopyWithWarnings(location.path, p(ctx.home, ".local", "bin", "effect-solutions"), services)
|
|
346
|
+
change("effect-solutions CLI ready (linked bun + effect-solutions into ~/.local/bin)")
|
|
336
347
|
} else {
|
|
337
348
|
warn(`effect-solutions installed but binary not found under '${gbin !== "" ? gbin : "<unknown>"}' — link it onto PATH manually`)
|
|
338
349
|
}
|
|
@@ -341,6 +352,7 @@ export function effectSolutionsInstall(ctx: Ctx): (mode: "install" | "upgrade",
|
|
|
341
352
|
}
|
|
342
353
|
|
|
343
354
|
function syncEffectSolutionsCli(ctx: Ctx): void {
|
|
355
|
+
const { warn } = ctx.services.logger
|
|
344
356
|
const settings = p(ctx.repoDir, "SoT", ".claude", "settings.json")
|
|
345
357
|
if (!existsSync(settings)) return
|
|
346
358
|
if (!/"effect-kit@docks"[ \t]*:[ \t]*true/.test(readFileSync(settings, "utf8"))) return
|
|
@@ -353,6 +365,7 @@ function syncEffectSolutionsCli(ctx: Ctx): void {
|
|
|
353
365
|
// ----------------------------------------------------- prune + snapshot ----
|
|
354
366
|
|
|
355
367
|
function reconcileRemovals(ctx: Ctx, manifest: string, snapshot: string): void {
|
|
368
|
+
const { change, echo, warn } = ctx.services.logger
|
|
356
369
|
if (!existsSync(snapshot)) {
|
|
357
370
|
if (ctx.dryRun) {
|
|
358
371
|
echo(
|
|
@@ -383,7 +396,10 @@ function reconcileRemovals(ctx: Ctx, manifest: string, snapshot: string): void {
|
|
|
383
396
|
}
|
|
384
397
|
}
|
|
385
398
|
|
|
386
|
-
if (removed > 0)
|
|
399
|
+
if (removed > 0) {
|
|
400
|
+
change(`Kit-managed skills removed (-${removed})`)
|
|
401
|
+
ctx.nextStepTriggers.skillsRestart = true
|
|
402
|
+
}
|
|
387
403
|
if (failed > 0) warn(`${failed} skill remove(s) failed — re-run with --prune or run: npx skills remove -g -y -a '*' -s <name>`)
|
|
388
404
|
}
|
|
389
405
|
|
|
@@ -392,18 +408,21 @@ function updateSnapshot(ctx: Ctx, manifest: string, snapshot: string): void {
|
|
|
392
408
|
|
|
393
409
|
mkdirSync(ctx.agentsDir, { recursive: true })
|
|
394
410
|
const sorted = [...new Set(readSlugs(manifest))].sort(compareCodepoints)
|
|
395
|
-
|
|
411
|
+
writeFileIfChanged(snapshot, sorted.length > 0 ? `${sorted.join("\n")}\n` : "")
|
|
396
412
|
}
|
|
397
413
|
|
|
398
414
|
// -------------------------------------------------------------- summary ----
|
|
399
415
|
|
|
400
416
|
export function skillsSummary(ctx: Ctx, state: SkillsState): void {
|
|
417
|
+
const { echo } = ctx.services.logger
|
|
401
418
|
echo(`Skills: ${p(ctx.agentsDir, "skills")}`)
|
|
402
419
|
if (!ctx.dryRun) {
|
|
403
420
|
echo(` ${state.present} universal skill(s) installed`)
|
|
404
421
|
}
|
|
405
422
|
}
|
|
406
423
|
|
|
407
|
-
export function skillsNextSteps():
|
|
408
|
-
|
|
424
|
+
export function skillsNextSteps(ctx: Ctx): Array<string> {
|
|
425
|
+
return ctx.verbose || ctx.nextStepTriggers.skillsRestart
|
|
426
|
+
? ["Restart Claude Code (and Codex) to discover newly installed universal skills."]
|
|
427
|
+
: []
|
|
409
428
|
}
|