docks-kit 0.1.2 → 0.1.4

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.
@@ -5,7 +5,7 @@
5
5
  * and the snapshot write.
6
6
  */
7
7
  import { spawnSync } from "node:child_process"
8
- import { cpSync, existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync, rmSync, statSync, symlinkSync, writeFileSync } from "node:fs"
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
10
  import { capture, commandExists, isExecutable, p, which } from "./exec"
11
11
  import type { Ctx } from "./index"
@@ -136,11 +136,17 @@ function healClaudeSymlink(ctx: Ctx, skillsDir: string, base: string): boolean {
136
136
  if (linkStat?.isSymbolicLink() === true) {
137
137
  const current = safeReadlink(claudeLink)
138
138
  if (current === relTarget) return false
139
+ // win32: `npx skills add` creates absolute symlinks/junctions — any link
140
+ // that RESOLVES to the canonical dir is healthy, not stale.
141
+ if (process.platform === "win32" && realpathEquals(claudeLink, canonical)) return false
139
142
  if (ctx.dryRun) {
140
143
  echo(`[dry-run] would replace stale Claude symlink: ~/.claude/skills/${base} -> ${current} (correct: ${relTarget})`)
141
144
  return true
142
145
  }
143
- rmSync(claudeLink, { force: true })
146
+ if (!removeLink(claudeLink)) {
147
+ warn(`could not remove stale link ~/.claude/skills/${base} — remove it manually, then re-run sync`)
148
+ return false
149
+ }
144
150
  } else if (linkStat !== undefined) {
145
151
  warn(`~/.claude/skills/${base} exists as a real path (not a symlink) — leaving alone; remove manually if it's stale`)
146
152
  return false
@@ -169,9 +175,34 @@ function safeReadlink(path: string): string {
169
175
  }
170
176
  }
171
177
 
178
+ function realpathEquals(a: string, b: string): boolean {
179
+ try {
180
+ return realpathSync(a) === realpathSync(b)
181
+ } catch {
182
+ return false
183
+ }
184
+ }
185
+
186
+ /**
187
+ * Remove a symlink/junction without crashing: Bun's rmSync throws EFAULT on
188
+ * win32 directory symlinks. unlink handles file symlinks, rmdir handles
189
+ * dir symlinks/junctions, rmSync is the POSIX catch-all.
190
+ */
191
+ function removeLink(path: string): boolean {
192
+ for (const rm of [() => unlinkSync(path), () => rmdirSync(path), () => rmSync(path, { force: true })]) {
193
+ try {
194
+ rm()
195
+ return true
196
+ } catch {
197
+ // try the next removal shape
198
+ }
199
+ }
200
+ return lstat(path) === undefined
201
+ }
202
+
172
203
  /** skills::_link_or_copy — real symlink preferred, copy fallback (Windows). */
173
204
  export function linkOrCopy(target: string, link: string): boolean {
174
- rmSync(link, { force: true })
205
+ removeLink(link)
175
206
  try {
176
207
  symlinkSync(target, link, process.platform === "win32" ? "dir" : undefined)
177
208
  } catch {
@@ -288,6 +319,15 @@ export function effectSolutionsInstall(ctx: Ctx): (mode: "install" | "upgrade",
288
319
  }
289
320
 
290
321
  const gbin = capture(bun, ["pm", "-g", "bin"])
322
+ // win32: bun writes an .exe shim (not the bare Unix name), and the
323
+ // ~/.local/bin link step below is Unix-only plumbing (non-interactive
324
+ // agent PATH) — bun's global bin is already the Windows PATH entry.
325
+ if (process.platform === "win32") {
326
+ const found = gbin !== "" && ["exe", "cmd", "bunx"].some((ext) => existsSync(p(gbin, `effect-solutions.${ext}`)))
327
+ if (found) log(`effect-solutions CLI ready (${gbin})`)
328
+ else warn(`effect-solutions installed but no shim found under '${gbin !== "" ? gbin : "<unknown>"}' — check bun pm -g bin`)
329
+ return 0
330
+ }
291
331
  if (gbin !== "" && isExecutable(p(gbin, "effect-solutions"))) {
292
332
  mkdirSync(p(ctx.home, ".local", "bin"), { recursive: true })
293
333
  linkOrCopy(bun, p(ctx.home, ".local", "bin", "bun"))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "docks-kit",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Portable AI coding agent config kit — SoT sync engine + typed CLI for Claude Code, Codex, and universal agent skills",
5
5
  "type": "module",
6
6
  "license": "MIT",