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.
Files changed (83) hide show
  1. package/AGENTS.md +36 -32
  2. package/cli/docs/omp-models.md +88 -53
  3. package/cli/src/argv.ts +38 -469
  4. package/cli/src/argvSurface.ts +371 -0
  5. package/cli/src/argvValidate.ts +122 -0
  6. package/cli/src/commands/docs.ts +57 -42
  7. package/cli/src/commands/harnesses.ts +38 -45
  8. package/cli/src/commands/model.ts +48 -53
  9. package/cli/src/commands/models.ts +24 -26
  10. package/cli/src/commands/omp.ts +132 -112
  11. package/cli/src/commands/plugins.ts +20 -20
  12. package/cli/src/commands/skills.ts +22 -20
  13. package/cli/src/commands/status.ts +87 -80
  14. package/cli/src/commands/sync.ts +113 -87
  15. package/cli/src/commands/toolchain.ts +21 -21
  16. package/cli/src/commands/update.ts +139 -117
  17. package/cli/src/efforts.ts +35 -34
  18. package/cli/src/engine-native/DESIGN.md +30 -30
  19. package/cli/src/engine-native/bun.ts +77 -55
  20. package/cli/src/engine-native/claudeLsp.ts +154 -0
  21. package/cli/src/engine-native/claudeOptionalPlugins.ts +113 -0
  22. package/cli/src/engine-native/claudePluginPasses.ts +348 -0
  23. package/cli/src/engine-native/claudeRemovals.ts +195 -0
  24. package/cli/src/engine-native/claudeRetired.ts +4 -4
  25. package/cli/src/engine-native/claudeRuntime.ts +94 -68
  26. package/cli/src/engine-native/claudeSettings.ts +174 -0
  27. package/cli/src/engine-native/claudeSettingsModifiers.ts +50 -54
  28. package/cli/src/engine-native/claudeSync.ts +176 -456
  29. package/cli/src/engine-native/codexConfig.ts +158 -0
  30. package/cli/src/engine-native/codexHooks.ts +91 -0
  31. package/cli/src/engine-native/codexPlugins.ts +290 -0
  32. package/cli/src/engine-native/codexStatus.ts +24 -0
  33. package/cli/src/engine-native/codexSync.ts +110 -568
  34. package/cli/src/engine-native/codexToml.ts +140 -138
  35. package/cli/src/engine-native/deps.ts +144 -107
  36. package/cli/src/engine-native/engineCtx.ts +126 -0
  37. package/cli/src/engine-native/exec.ts +98 -87
  38. package/cli/src/engine-native/failures.ts +3 -3
  39. package/cli/src/engine-native/harnesses.ts +62 -72
  40. package/cli/src/engine-native/index.ts +34 -315
  41. package/cli/src/engine-native/jq.ts +25 -24
  42. package/cli/src/engine-native/logger.ts +98 -100
  43. package/cli/src/engine-native/models.ts +50 -45
  44. package/cli/src/engine-native/modes.ts +83 -80
  45. package/cli/src/engine-native/ompFileDeploy.ts +111 -0
  46. package/cli/src/engine-native/ompMarketplace.ts +136 -0
  47. package/cli/src/engine-native/ompOverlay.ts +67 -64
  48. package/cli/src/engine-native/ompPaths.ts +49 -38
  49. package/cli/src/engine-native/ompPlugins.ts +170 -0
  50. package/cli/src/engine-native/ompSync.ts +64 -408
  51. package/cli/src/engine-native/ompYaml.ts +43 -41
  52. package/cli/src/engine-native/os/darwin.ts +5 -5
  53. package/cli/src/engine-native/os/index.ts +15 -15
  54. package/cli/src/engine-native/os/linux.ts +5 -5
  55. package/cli/src/engine-native/os/posix.ts +28 -24
  56. package/cli/src/engine-native/os/targets.ts +17 -17
  57. package/cli/src/engine-native/os/types.ts +31 -31
  58. package/cli/src/engine-native/os/windows.ts +61 -61
  59. package/cli/src/engine-native/parseArgs.ts +122 -372
  60. package/cli/src/engine-native/parseHelp.ts +66 -0
  61. package/cli/src/engine-native/parseModifiers.ts +222 -0
  62. package/cli/src/engine-native/services.ts +34 -34
  63. package/cli/src/engine-native/settings.ts +15 -15
  64. package/cli/src/engine-native/sharedTypes.d.ts +46 -0
  65. package/cli/src/engine-native/skillsInstall.ts +105 -0
  66. package/cli/src/engine-native/skillsLinks.ts +203 -0
  67. package/cli/src/engine-native/skillsManifest.ts +16 -0
  68. package/cli/src/engine-native/skillsPrune.ts +108 -0
  69. package/cli/src/engine-native/skillsSync.ts +28 -362
  70. package/cli/src/engine-native/syncConcurrency.ts +55 -0
  71. package/cli/src/engine-native/syncDispatch.ts +141 -0
  72. package/cli/src/engine-native/toolchain.ts +65 -62
  73. package/cli/src/engine.ts +65 -57
  74. package/cli/src/generated/sotPayload.ts +7 -7
  75. package/cli/src/kitHome.ts +44 -40
  76. package/cli/src/main.ts +63 -56
  77. package/cli/src/manifests.ts +53 -54
  78. package/cli/src/md.d.ts +2 -2
  79. package/cli/src/payload.ts +9 -9
  80. package/cli/src/services.ts +21 -13
  81. package/cli/tsconfig.json +4 -1
  82. package/package.json +9 -2
  83. package/cli/src/engine-native/claudePlugins.ts +0 -510
@@ -0,0 +1,203 @@
1
+ /**
2
+ * EngineNative skill link mechanics: Claude per-tool symlink healing plus the
3
+ * directory-link-or-marked-copy fallback. Split from skillsSync.ts; the
4
+ * install pass (skillsInstall.ts) and the prune pass (skillsPrune.ts) share
5
+ * the probe and removal helpers exported here.
6
+ */
7
+ import {
8
+ cpSync,
9
+ existsSync,
10
+ lstatSync,
11
+ mkdirSync,
12
+ readlinkSync,
13
+ rmSync,
14
+ statSync,
15
+ symlinkSync,
16
+ writeFileSync,
17
+ } from "node:fs";
18
+ import { dirname, relative, resolve } from "node:path";
19
+ import { p } from "./exec";
20
+ import type { Ctx } from "./index";
21
+ import { hostOs, type DirectoryLinkKind } from "./os";
22
+ import type { EngineServices } from "./services";
23
+
24
+ export type LinkOutcome = "symlink" | "junction" | "copy" | "failed";
25
+
26
+ /** Lets heal and prune distinguish a kit-owned copy from a user's real directory. */
27
+ export const COPY_MARKER = ".docks-kit-copied-skill";
28
+
29
+ export function isDir(path: string): boolean {
30
+ try {
31
+ return statSync(path).isDirectory();
32
+ } catch {
33
+ return false;
34
+ }
35
+ }
36
+
37
+ /** skills::heal_claude_symlink — true when a heal occurred. */
38
+ export function healClaudeSymlink(ctx: Ctx, skillsDir: string, base: string): boolean {
39
+ const { echo, warn } = ctx.services.logger;
40
+ const canonical = p(skillsDir, base);
41
+ const claudeSkillsDir = p(ctx.home, ".claude", "skills");
42
+ const claudeLink = p(claudeSkillsDir, base);
43
+ const relTarget = relative(dirname(claudeLink), canonical);
44
+
45
+ if (!isDir(canonical)) return false;
46
+
47
+ const linkStat = lstat(claudeLink);
48
+ if (linkStat?.isSymbolicLink() === true) {
49
+ const current = safeReadlink(claudeLink);
50
+ if (current === relTarget) return false;
51
+ if (ctx.dryRun) {
52
+ echo(
53
+ `[dry-run] would replace stale Claude symlink: ~/.claude/skills/${base} -> ${current} (correct: ${relTarget})`,
54
+ );
55
+ return true;
56
+ }
57
+ if (!removeLink(claudeLink)) {
58
+ warn(
59
+ `could not remove stale link ~/.claude/skills/${base} — remove it manually, then re-run sync`,
60
+ );
61
+ return false;
62
+ }
63
+ } else if (linkStat !== undefined) {
64
+ if (!isKitOwnedCopy(claudeLink)) {
65
+ warn(
66
+ `~/.claude/skills/${base} exists as a real path (not a symlink) — leaving alone; remove manually if it's stale`,
67
+ );
68
+ return false;
69
+ }
70
+ if (ctx.dryRun) {
71
+ echo(
72
+ `[dry-run] would replace kit-created Claude copy: ~/.claude/skills/${base} -> ${relTarget}`,
73
+ );
74
+ return true;
75
+ }
76
+ if (!removeKitOwnedCopy(claudeLink)) {
77
+ warn(
78
+ `could not remove kit-created copy ~/.claude/skills/${base} — remove it manually, then re-run sync`,
79
+ );
80
+ return false;
81
+ }
82
+ } else if (ctx.dryRun) {
83
+ echo(`[dry-run] would create missing Claude symlink: ~/.claude/skills/${base} -> ${relTarget}`);
84
+ return true;
85
+ }
86
+
87
+ mkdirSync(claudeSkillsDir, { recursive: true });
88
+ return linkOrCopyWithWarnings(relTarget, claudeLink, ctx.services) !== "failed";
89
+ }
90
+
91
+ export function lstat(path: string): ReturnType<typeof lstatSync> | undefined {
92
+ try {
93
+ return lstatSync(path);
94
+ } catch {
95
+ return undefined;
96
+ }
97
+ }
98
+
99
+ /**
100
+ * A link only counts when it RESOLVES to the skill directory. Windows picks a
101
+ * symlink's file-or-directory type by autodetecting the target against the
102
+ * process working directory, not the link's own directory, so a relative
103
+ * target can yield a symlink that exists but resolves to nothing. Checking
104
+ * resolution is what makes the next mechanism in the chain reachable.
105
+ */
106
+ function linksToDirectory(path: string): boolean {
107
+ if (lstat(path)?.isSymbolicLink() !== true) return false;
108
+ try {
109
+ return statSync(path).isDirectory();
110
+ } catch {
111
+ return false;
112
+ }
113
+ }
114
+
115
+ export function isKitOwnedCopy(path: string): boolean {
116
+ return lstat(path)?.isDirectory() === true && existsSync(p(path, COPY_MARKER));
117
+ }
118
+
119
+ export function removeKitOwnedCopy(path: string): boolean {
120
+ if (!isKitOwnedCopy(path)) return false;
121
+ try {
122
+ rmSync(path, { recursive: true, force: true });
123
+ return true;
124
+ } catch {
125
+ return lstat(path) === undefined;
126
+ }
127
+ }
128
+
129
+ function safeReadlink(path: string): string {
130
+ try {
131
+ return readlinkSync(path);
132
+ } catch {
133
+ return "";
134
+ }
135
+ }
136
+
137
+ /** Remove a symlink without touching a real directory. */
138
+ export function removeLink(path: string): boolean {
139
+ try {
140
+ rmSync(path, { force: true });
141
+ return true;
142
+ } catch {
143
+ return lstat(path) === undefined;
144
+ }
145
+ }
146
+
147
+ /** skills::_link_or_copy — try directory links in host order, then a marked copy. */
148
+ export function linkOrCopy(
149
+ target: string,
150
+ link: string,
151
+ kinds: ReadonlyArray<DirectoryLinkKind> = hostOs().directoryLinkKinds,
152
+ ): LinkOutcome {
153
+ const resolvedLink = resolve(link);
154
+ const absoluteTarget = resolve(dirname(resolvedLink), target);
155
+ if (absoluteTarget === resolvedLink) return "symlink";
156
+ removeLink(link);
157
+
158
+ for (const kind of kinds) {
159
+ try {
160
+ if (kind === "symlink") {
161
+ symlinkSync(target, link);
162
+ } else {
163
+ symlinkSync(absoluteTarget, link, "junction");
164
+ }
165
+ if (linksToDirectory(link)) return kind;
166
+ } catch {
167
+ // The runtime decides whether each mechanism works; try the next one.
168
+ }
169
+ removeLink(link);
170
+ }
171
+
172
+ const copyDestinationWasAbsent = lstat(link) === undefined;
173
+ try {
174
+ cpSync(absoluteTarget, link, { recursive: true });
175
+ writeFileSync(p(link, COPY_MARKER), "");
176
+ return "copy";
177
+ } catch {
178
+ if (copyDestinationWasAbsent) {
179
+ try {
180
+ rmSync(link, { recursive: true, force: true });
181
+ } catch {
182
+ // The outcome remains failed; a later sync can retry the destination.
183
+ }
184
+ }
185
+ return "failed";
186
+ }
187
+ }
188
+
189
+ function linkOrCopyWithWarnings(
190
+ target: string,
191
+ link: string,
192
+ services: EngineServices,
193
+ ): LinkOutcome {
194
+ const outcome = linkOrCopy(target, link);
195
+ if (outcome === "copy") {
196
+ services.logger.warn(
197
+ `created copy fallback ${link} because directory linking is unavailable — a later sync will restore a real link once linking works`,
198
+ );
199
+ } else if (outcome === "failed") {
200
+ services.logger.warn(`could not create symlink ${link}`);
201
+ }
202
+ return outcome;
203
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * EngineNative universal-skill manifest parsing: cleaned slugs, one per line.
3
+ * Split from skillsSync.ts. Pure logic with no process state.
4
+ */
5
+
6
+ /** skills::_normalize_manifest — cleaned slugs, one per line. */
7
+ export function normalizeManifest(content: string): Array<string> {
8
+ const out: Array<string> = [];
9
+ for (const line of content.split("\n")) {
10
+ if (/^[ \t]*#/.test(line)) continue;
11
+ if (/^[ \t]*$/.test(line)) continue;
12
+ const cleaned = line.replace(/[ \t]*#.*$/, "").replace(/[ \t\r]+/g, "");
13
+ if (cleaned.length > 0) out.push(cleaned);
14
+ }
15
+ return out;
16
+ }
@@ -0,0 +1,108 @@
1
+ /**
2
+ * EngineNative kit-managed skill prune (`--prune` reconcile against the
3
+ * snapshot) and snapshot write. Split from skillsSync.ts; the orchestration
4
+ * still lives there.
5
+ */
6
+ import { existsSync, mkdirSync, readFileSync } from "node:fs";
7
+ import { p, spawnProcess, writeTextIfChanged } from "./exec";
8
+ import type { Ctx } from "./index";
9
+ import { compareCodepoints } from "./jq";
10
+ import { skillsCli } from "./skillsInstall";
11
+ import { isKitOwnedCopy, lstat, removeKitOwnedCopy, removeLink } from "./skillsLinks";
12
+ import { normalizeManifest } from "./skillsManifest";
13
+
14
+ // ----------------------------------------------------- prune + snapshot ----
15
+
16
+ function readSlugs(file: string): Array<string> {
17
+ return existsSync(file) ? normalizeManifest(readFileSync(file, "utf8")) : [];
18
+ }
19
+
20
+ export async function reconcileRemovals(
21
+ ctx: Ctx,
22
+ manifest: string,
23
+ snapshot: string,
24
+ ): Promise<Array<string>> {
25
+ const { change, clearProgress, echo, progress, warn } = ctx.services.logger;
26
+ if (!existsSync(snapshot)) {
27
+ if (ctx.dryRun) {
28
+ echo(
29
+ `[dry-run] (--prune) no kit-managed-skills snapshot yet; first real sync writes ${snapshot}, then future --prune runs reconcile against it`,
30
+ );
31
+ }
32
+ return [];
33
+ }
34
+
35
+ const current = normalizeManifest(manifest);
36
+ const currentBases = new Set(current.map((slug) => slug.slice(slug.lastIndexOf("/") + 1)));
37
+ let removed = 0;
38
+ let failed = 0;
39
+ const failedSlugs: Array<string> = [];
40
+ for (const slug of readSlugs(snapshot)) {
41
+ if (current.includes(slug)) continue;
42
+ const base = slug.slice(slug.lastIndexOf("/") + 1);
43
+ if (currentBases.has(base)) continue;
44
+ const claudeEntry = p(ctx.home, ".claude", "skills", base);
45
+ const managedClaudeEntry =
46
+ lstat(claudeEntry)?.isSymbolicLink() === true || isKitOwnedCopy(claudeEntry);
47
+ if (ctx.dryRun) {
48
+ echo(`[dry-run] kit-managed skill no longer in SoT — would remove: ${base}`);
49
+ if (managedClaudeEntry) {
50
+ echo(`[dry-run] kit-managed Claude skill entry — would remove: ~/.claude/skills/${base}`);
51
+ }
52
+ continue;
53
+ }
54
+ progress(`Removing universal skill ${base}...`);
55
+ const res = await spawnProcess(
56
+ "npx",
57
+ ["--yes", skillsCli(ctx), "remove", "--global", base, "-y"],
58
+ {
59
+ stdio: "ignore",
60
+ },
61
+ );
62
+ clearProgress();
63
+ if (res.error !== undefined || res.exitCode !== 0) {
64
+ warn(`Failed to remove kit-managed skill: ${base}`);
65
+ failed++;
66
+ failedSlugs.push(slug);
67
+ continue;
68
+ }
69
+ if (managedClaudeEntry) {
70
+ const entryStat = lstat(claudeEntry);
71
+ const removedClaudeEntry =
72
+ entryStat === undefined ||
73
+ (entryStat.isSymbolicLink() ? removeLink(claudeEntry) : removeKitOwnedCopy(claudeEntry));
74
+ if (!removedClaudeEntry) {
75
+ warn(`Failed to remove kit-managed Claude skill entry: ${base}`);
76
+ failed++;
77
+ failedSlugs.push(slug);
78
+ continue;
79
+ }
80
+ }
81
+ removed++;
82
+ }
83
+
84
+ if (removed > 0) {
85
+ change(`Kit-managed skills removed (-${removed})`);
86
+ ctx.nextStepTriggers.skillsRestart = true;
87
+ }
88
+ if (failed > 0)
89
+ warn(
90
+ `${failed} skill remove(s) failed — re-run with --prune or run: npx skills remove --global <name> -y`,
91
+ );
92
+ return failedSlugs;
93
+ }
94
+
95
+ export function updateSnapshot(
96
+ ctx: Ctx,
97
+ manifest: string,
98
+ snapshot: string,
99
+ failedRemovals: ReadonlyArray<string>,
100
+ ): void {
101
+ if (ctx.dryRun) return;
102
+
103
+ mkdirSync(ctx.agentsDir, { recursive: true });
104
+ const sorted = [...new Set([...normalizeManifest(manifest), ...failedRemovals])].sort(
105
+ compareCodepoints,
106
+ );
107
+ writeTextIfChanged(snapshot, sorted.length > 0 ? `${sorted.join("\n")}\n` : "");
108
+ }