docks-kit 0.17.0 → 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 (84) hide show
  1. package/AGENTS.md +37 -33
  2. package/README.md +1 -0
  3. package/cli/docs/omp-models.md +114 -58
  4. package/cli/src/argv.ts +38 -469
  5. package/cli/src/argvSurface.ts +371 -0
  6. package/cli/src/argvValidate.ts +122 -0
  7. package/cli/src/commands/docs.ts +57 -42
  8. package/cli/src/commands/harnesses.ts +38 -45
  9. package/cli/src/commands/model.ts +48 -53
  10. package/cli/src/commands/models.ts +24 -26
  11. package/cli/src/commands/omp.ts +178 -88
  12. package/cli/src/commands/plugins.ts +20 -20
  13. package/cli/src/commands/skills.ts +22 -20
  14. package/cli/src/commands/status.ts +87 -80
  15. package/cli/src/commands/sync.ts +113 -87
  16. package/cli/src/commands/toolchain.ts +21 -21
  17. package/cli/src/commands/update.ts +139 -117
  18. package/cli/src/efforts.ts +35 -34
  19. package/cli/src/engine-native/DESIGN.md +30 -30
  20. package/cli/src/engine-native/bun.ts +77 -55
  21. package/cli/src/engine-native/claudeLsp.ts +154 -0
  22. package/cli/src/engine-native/claudeOptionalPlugins.ts +113 -0
  23. package/cli/src/engine-native/claudePluginPasses.ts +348 -0
  24. package/cli/src/engine-native/claudeRemovals.ts +195 -0
  25. package/cli/src/engine-native/claudeRetired.ts +4 -4
  26. package/cli/src/engine-native/claudeRuntime.ts +94 -68
  27. package/cli/src/engine-native/claudeSettings.ts +174 -0
  28. package/cli/src/engine-native/claudeSettingsModifiers.ts +50 -54
  29. package/cli/src/engine-native/claudeSync.ts +176 -456
  30. package/cli/src/engine-native/codexConfig.ts +158 -0
  31. package/cli/src/engine-native/codexHooks.ts +91 -0
  32. package/cli/src/engine-native/codexPlugins.ts +290 -0
  33. package/cli/src/engine-native/codexStatus.ts +24 -0
  34. package/cli/src/engine-native/codexSync.ts +110 -568
  35. package/cli/src/engine-native/codexToml.ts +140 -138
  36. package/cli/src/engine-native/deps.ts +144 -107
  37. package/cli/src/engine-native/engineCtx.ts +126 -0
  38. package/cli/src/engine-native/exec.ts +98 -87
  39. package/cli/src/engine-native/failures.ts +3 -3
  40. package/cli/src/engine-native/harnesses.ts +62 -72
  41. package/cli/src/engine-native/index.ts +34 -315
  42. package/cli/src/engine-native/jq.ts +25 -24
  43. package/cli/src/engine-native/logger.ts +98 -100
  44. package/cli/src/engine-native/models.ts +50 -45
  45. package/cli/src/engine-native/modes.ts +83 -80
  46. package/cli/src/engine-native/ompFileDeploy.ts +111 -0
  47. package/cli/src/engine-native/ompMarketplace.ts +136 -0
  48. package/cli/src/engine-native/ompOverlay.ts +93 -53
  49. package/cli/src/engine-native/ompPaths.ts +49 -38
  50. package/cli/src/engine-native/ompPlugins.ts +170 -0
  51. package/cli/src/engine-native/ompSync.ts +64 -408
  52. package/cli/src/engine-native/ompYaml.ts +43 -41
  53. package/cli/src/engine-native/os/darwin.ts +5 -5
  54. package/cli/src/engine-native/os/index.ts +15 -15
  55. package/cli/src/engine-native/os/linux.ts +5 -5
  56. package/cli/src/engine-native/os/posix.ts +28 -24
  57. package/cli/src/engine-native/os/targets.ts +17 -17
  58. package/cli/src/engine-native/os/types.ts +31 -31
  59. package/cli/src/engine-native/os/windows.ts +61 -61
  60. package/cli/src/engine-native/parseArgs.ts +122 -372
  61. package/cli/src/engine-native/parseHelp.ts +66 -0
  62. package/cli/src/engine-native/parseModifiers.ts +222 -0
  63. package/cli/src/engine-native/services.ts +34 -34
  64. package/cli/src/engine-native/settings.ts +15 -15
  65. package/cli/src/engine-native/sharedTypes.d.ts +46 -0
  66. package/cli/src/engine-native/skillsInstall.ts +105 -0
  67. package/cli/src/engine-native/skillsLinks.ts +203 -0
  68. package/cli/src/engine-native/skillsManifest.ts +16 -0
  69. package/cli/src/engine-native/skillsPrune.ts +108 -0
  70. package/cli/src/engine-native/skillsSync.ts +28 -362
  71. package/cli/src/engine-native/syncConcurrency.ts +55 -0
  72. package/cli/src/engine-native/syncDispatch.ts +141 -0
  73. package/cli/src/engine-native/toolchain.ts +65 -62
  74. package/cli/src/engine.ts +65 -57
  75. package/cli/src/generated/sotPayload.ts +7 -7
  76. package/cli/src/kitHome.ts +44 -40
  77. package/cli/src/main.ts +63 -56
  78. package/cli/src/manifests.ts +53 -54
  79. package/cli/src/md.d.ts +2 -2
  80. package/cli/src/payload.ts +9 -9
  81. package/cli/src/services.ts +21 -13
  82. package/cli/tsconfig.json +4 -1
  83. package/package.json +9 -2
  84. package/cli/src/engine-native/claudePlugins.ts +0 -510
@@ -0,0 +1,222 @@
1
+ /**
2
+ * Modifier and effort mapping for the EngineNative flag layer: deploy-time
3
+ * modifier metadata, scalar-modifier setters, compact-window and opt-in
4
+ * plugin parsing, and post-parse modifier validation. Pure apart from the
5
+ * injected ctx seam; help output and the early-exit error come from
6
+ * parseHelp.ts.
7
+ */
8
+
9
+ import type { Ctx, ModifierFlag } from "./index";
10
+ import type { ScalarModifierFlag } from "./sharedTypes";
11
+ import {
12
+ CLAUDE_ADVISOR_STATES,
13
+ advisorCatalog,
14
+ advisorValueGrammar,
15
+ effortCatalog,
16
+ effortValueGrammar,
17
+ isEffortModifierValue,
18
+ } from "../efforts";
19
+ import { printModels, validateClaudeModel, validateCodexModel } from "./models";
20
+ import { ExitError, printCatalog } from "./parseHelp";
21
+
22
+ export const KNOWN_CLAUDE_OPTIN_PLUGINS = ["supabase", "n8n"];
23
+
24
+ interface ModifierMetadata {
25
+ readonly target: "claude" | "codex";
26
+ readonly ignoredWarning: string;
27
+ readonly hasValue: (ctx: Ctx) => boolean;
28
+ readonly clear: (ctx: Ctx) => void;
29
+ }
30
+
31
+ const MODIFIER_METADATA = {
32
+ "--claude-model": {
33
+ target: "claude",
34
+ ignoredWarning: "--claude-model ignored: claude target not selected",
35
+ hasValue: (ctx) => ctx.claudeModel !== "",
36
+ clear: (ctx) => {
37
+ ctx.claudeModel = "";
38
+ },
39
+ },
40
+ "--claude-effort": {
41
+ target: "claude",
42
+ ignoredWarning: "--claude-effort ignored: claude target not selected",
43
+ hasValue: (ctx) => ctx.claudeEffort !== "",
44
+ clear: (ctx) => {
45
+ ctx.claudeEffort = "";
46
+ },
47
+ },
48
+ "--claude-advisor": {
49
+ target: "claude",
50
+ ignoredWarning: "--claude-advisor ignored: claude target not selected",
51
+ hasValue: (ctx) => ctx.claudeAdvisor !== "",
52
+ clear: (ctx) => {
53
+ ctx.claudeAdvisor = "";
54
+ },
55
+ },
56
+ "--claude-compact-window": {
57
+ target: "claude",
58
+ ignoredWarning: "--claude-compact-window ignored: claude target not selected",
59
+ hasValue: (ctx) => ctx.claudeCompactWindow !== "",
60
+ clear: (ctx) => {
61
+ ctx.claudeCompactWindow = "";
62
+ },
63
+ },
64
+ "--claude-permissive": {
65
+ target: "claude",
66
+ ignoredWarning: "--claude-permissive ignored: claude target not selected",
67
+ hasValue: (ctx) => ctx.claudePermissive,
68
+ clear: (ctx) => {
69
+ ctx.claudePermissive = false;
70
+ },
71
+ },
72
+ "--claude-plugin": {
73
+ target: "claude",
74
+ ignoredWarning: "--claude-plugin ignored: claude target not selected",
75
+ hasValue: (ctx) => ctx.claudePlugins.length > 0,
76
+ clear: (ctx) => {
77
+ ctx.claudePlugins = [];
78
+ },
79
+ },
80
+ "--codex-model": {
81
+ target: "codex",
82
+ ignoredWarning: "--codex-model ignored: codex target not selected",
83
+ hasValue: (ctx) => ctx.codexModel !== "",
84
+ clear: (ctx) => {
85
+ ctx.codexModel = "";
86
+ },
87
+ },
88
+ "--codex-effort": {
89
+ target: "codex",
90
+ ignoredWarning: "--codex-effort ignored: codex target not selected",
91
+ hasValue: (ctx) => ctx.codexEffort !== "",
92
+ clear: (ctx) => {
93
+ ctx.codexEffort = "";
94
+ },
95
+ },
96
+ } satisfies Record<ModifierFlag, ModifierMetadata>;
97
+
98
+ export type { ScalarModifierFlag };
99
+
100
+ const SCALAR_MODIFIER_FLAGS: Record<ScalarModifierFlag, true> = {
101
+ "--claude-model": true,
102
+ "--claude-effort": true,
103
+ "--claude-advisor": true,
104
+ "--codex-model": true,
105
+ "--codex-effort": true,
106
+ };
107
+
108
+ /** common::parse_compact_window — normalized tokens or undefined on junk. */
109
+ export function parseCompactWindow(v: string): string | undefined {
110
+ if (/[kK]$/.test(v)) {
111
+ const n = v.slice(0, -1);
112
+ if (!/^[0-9]+$/.test(n)) return undefined;
113
+ return String(parseInt(n, 10) * 1000);
114
+ }
115
+ return /^[0-9]+$/.test(v) ? v : undefined;
116
+ }
117
+
118
+ export function parseClaudePlugin(name: string, err: (message: string) => void): string {
119
+ if (!KNOWN_CLAUDE_OPTIN_PLUGINS.includes(name)) {
120
+ err(`Unknown opt-in plugin '${name}'. Known: ${KNOWN_CLAUDE_OPTIN_PLUGINS.join(", ")}`);
121
+ throw new ExitError(2);
122
+ }
123
+ return name;
124
+ }
125
+
126
+ export function markModifier(ctx: Ctx, flag: ModifierFlag): void {
127
+ const flags = ctx.modifierFlags ?? new Set<ModifierFlag>();
128
+ flags.add(flag);
129
+ ctx.modifierFlags = flags;
130
+ }
131
+
132
+ export function addClaudePlugin(ctx: Ctx, name: string): void {
133
+ if (name === "") {
134
+ printCatalog(
135
+ ctx,
136
+ `Available Claude optional plugins:\n${KNOWN_CLAUDE_OPTIN_PLUGINS.map((plugin) => ` ${plugin}`).join("\n")}`,
137
+ );
138
+ ctx.services.logger.err(
139
+ `Invalid Claude plugin '' — valid: ${KNOWN_CLAUDE_OPTIN_PLUGINS.join("|")}`,
140
+ );
141
+ throw new ExitError(2);
142
+ }
143
+ ctx.claudePlugins.push(parseClaudePlugin(name, ctx.services.logger.err));
144
+ markModifier(ctx, "--claude-plugin");
145
+ }
146
+
147
+ export function setModifier(ctx: Ctx, flag: ScalarModifierFlag, value: string): void {
148
+ switch (flag) {
149
+ case "--claude-model":
150
+ ctx.claudeModel = value;
151
+ break;
152
+ case "--claude-effort":
153
+ ctx.claudeEffort = value;
154
+ break;
155
+ case "--claude-advisor":
156
+ ctx.claudeAdvisor = value;
157
+ break;
158
+ case "--codex-model":
159
+ ctx.codexModel = value;
160
+ break;
161
+ case "--codex-effort":
162
+ ctx.codexEffort = value;
163
+ break;
164
+ }
165
+ markModifier(ctx, flag);
166
+ }
167
+
168
+ export function isScalarModifierFlag(value: string): value is ScalarModifierFlag {
169
+ return SCALAR_MODIFIER_FLAGS[value as ScalarModifierFlag] === true;
170
+ }
171
+
172
+ export function validateModifierFlags(ctx: Ctx): void {
173
+ const { err, warn } = ctx.services.logger;
174
+ const supplied = (flag: ModifierFlag): boolean =>
175
+ MODIFIER_METADATA[flag].hasValue(ctx) || ctx.modifierFlags?.has(flag) === true;
176
+
177
+ for (const flag of Object.keys(MODIFIER_METADATA) as Array<ModifierFlag>) {
178
+ const metadata = MODIFIER_METADATA[flag];
179
+ const targetSelected = metadata.target === "claude" ? ctx.syncClaude : ctx.syncCodex;
180
+ if (!targetSelected && supplied(flag)) {
181
+ warn(metadata.ignoredWarning);
182
+ metadata.clear(ctx);
183
+ ctx.modifierFlags?.delete(flag);
184
+ }
185
+ }
186
+
187
+ if (supplied("--claude-model")) {
188
+ if (!validateClaudeModel(ctx, ctx.claudeModel)) {
189
+ printModels(ctx, "claude");
190
+ err(`Invalid Claude model '${ctx.claudeModel}' — use an alias above or a full claude-* ID`);
191
+ throw new ExitError(2);
192
+ }
193
+ }
194
+ if (supplied("--claude-effort")) {
195
+ if (!isEffortModifierValue("claude", ctx.claudeEffort)) {
196
+ printCatalog(ctx, effortCatalog("claude"));
197
+ err(`Invalid Claude effort '${ctx.claudeEffort}' — valid: ${effortValueGrammar("claude")}`);
198
+ throw new ExitError(2);
199
+ }
200
+ }
201
+ if (supplied("--claude-advisor")) {
202
+ if (!CLAUDE_ADVISOR_STATES.some((state) => state === ctx.claudeAdvisor)) {
203
+ printCatalog(ctx, advisorCatalog());
204
+ err(`Invalid Claude advisor state '${ctx.claudeAdvisor}' — valid: ${advisorValueGrammar()}`);
205
+ throw new ExitError(2);
206
+ }
207
+ }
208
+ if (supplied("--codex-model")) {
209
+ if (!validateCodexModel(ctx, ctx.codexModel)) {
210
+ printModels(ctx, "codex");
211
+ err(`Invalid Codex model '${ctx.codexModel}' — must match ^[A-Za-z0-9._-]+$`);
212
+ throw new ExitError(2);
213
+ }
214
+ }
215
+ if (supplied("--codex-effort")) {
216
+ if (!isEffortModifierValue("codex", ctx.codexEffort)) {
217
+ printCatalog(ctx, effortCatalog("codex"));
218
+ err(`Invalid Codex effort '${ctx.codexEffort}' — valid: ${effortValueGrammar("codex")}`);
219
+ throw new ExitError(2);
220
+ }
221
+ }
222
+ }
@@ -14,71 +14,71 @@ import {
14
14
  type DependencySpec,
15
15
  type ProbeExecutor,
16
16
  type ProbeResult,
17
- type ToolId
18
- } from "./deps"
19
- import { makeLogger, type Logger, type LoggerSinks } from "./logger"
20
- import { platformName, rawPlatform, type PlatformName } from "./os"
17
+ type ToolId,
18
+ } from "./deps";
19
+ import { makeLogger, type Logger, type LoggerSinks } from "./logger";
20
+ import { platformName, rawPlatform, type PlatformName } from "./os";
21
21
 
22
- export type { Logger } from "./logger"
22
+ export type { Logger } from "./logger";
23
23
 
24
24
  export interface DependencyManager {
25
- readonly spec: (id: ToolId) => DependencySpec
26
- readonly probe: (id: ToolId) => ProbeResult
27
- readonly version: (id: ToolId) => Promise<string>
28
- readonly path: (id: ToolId) => Promise<string>
29
- readonly warnMissing: (id: ToolId, logger: Logger, context?: string) => void
25
+ readonly spec: (id: ToolId) => DependencySpec;
26
+ readonly probe: (id: ToolId) => ProbeResult;
27
+ readonly version: (id: ToolId) => Promise<string>;
28
+ readonly path: (id: ToolId) => Promise<string>;
29
+ readonly warnMissing: (id: ToolId, logger: Logger, context?: string) => void;
30
30
  }
31
31
 
32
32
  export interface Platform {
33
- readonly raw: () => NodeJS.Platform
34
- readonly name: () => PlatformName
33
+ readonly raw: () => NodeJS.Platform;
34
+ readonly name: () => PlatformName;
35
35
  }
36
36
 
37
37
  export interface EngineServices {
38
- readonly logger: Logger
39
- readonly deps: DependencyManager
40
- readonly platform: Platform
38
+ readonly logger: Logger;
39
+ readonly deps: DependencyManager;
40
+ readonly platform: Platform;
41
41
  }
42
42
 
43
43
  export interface EngineServiceOptions {
44
- readonly sinks?: LoggerSinks
44
+ readonly sinks?: LoggerSinks;
45
45
  }
46
46
 
47
47
  /** Platform view over an injectable platform id. */
48
48
  export const makePlatform = (pf: NodeJS.Platform = rawPlatform()): Platform => ({
49
49
  raw: () => pf,
50
- name: () => platformName(pf)
51
- })
50
+ name: () => platformName(pf),
51
+ });
52
52
 
53
53
  /** DependencyManager whose hints default to the INJECTED platform, not the host. */
54
54
  export const makeDependencyManager = (
55
55
  platform: Platform,
56
- exec: ProbeExecutor = defaultProbeExecutor
56
+ exec: ProbeExecutor = defaultProbeExecutor,
57
57
  ): DependencyManager => {
58
- const warned = new Set<ToolId>()
58
+ const warned = new Set<ToolId>();
59
59
  return {
60
60
  spec: (id) => {
61
- const s = DEPENDENCIES[id]
62
- return { ...s, installHint: (pf = platform.raw()) => s.installHint(pf) }
61
+ const s = DEPENDENCIES[id];
62
+ return { ...s, installHint: (pf = platform.raw()) => s.installHint(pf) };
63
63
  },
64
64
  probe: (id) => resolveDependency(DEPENDENCIES[id], exec, platform.raw()),
65
65
  version: (id) => resolveVersion(DEPENDENCIES[id], exec),
66
66
  path: (id) => resolvePath(DEPENDENCIES[id], exec, platform.raw()),
67
67
  warnMissing: (id, logger, context) => {
68
- if (warned.has(id)) return
69
- warned.add(id)
70
- const suffix = context !== undefined && context !== "" ? ` (${context})` : ""
71
- logger.warn(`${id} not installed — ${DEPENDENCIES[id].installHint(platform.raw())}${suffix}`)
72
- }
73
- }
74
- }
68
+ if (warned.has(id)) return;
69
+ warned.add(id);
70
+ const suffix = context !== undefined && context !== "" ? ` (${context})` : "";
71
+ logger.warn(`${id} not installed — ${DEPENDENCIES[id].installHint(platform.raw())}${suffix}`);
72
+ },
73
+ };
74
+ };
75
75
 
76
76
  export const makeEngineServices = (opts?: EngineServiceOptions): EngineServices => {
77
- const platform = makePlatform()
78
- const logger = makeLogger(opts?.sinks ?? {})
77
+ const platform = makePlatform();
78
+ const logger = makeLogger(opts?.sinks ?? {});
79
79
  return {
80
80
  logger,
81
81
  deps: makeDependencyManager(platform),
82
- platform
83
- }
84
- }
82
+ platform,
83
+ };
84
+ };
@@ -4,11 +4,11 @@
4
4
  * log line) lives in the sync orchestrator. jq-oracle equivalence is covered by
5
5
  * cli/test/unit/settings.test.ts and deployed behavior by the golden suites.
6
6
  */
7
- import { deepMerge, isObject, uniqueStrings, type Json } from "./jq"
7
+ import { deepMerge, isObject, uniqueStrings, type Json } from "./jq";
8
8
 
9
9
  /** `$user * $repo` — SoT keys win, permissions arrays replaced wholesale. */
10
10
  export function reconcileSettings(repo: Json, user: Json): Json {
11
- return deepMerge(user, repo)
11
+ return deepMerge(user, repo);
12
12
  }
13
13
 
14
14
  /**
@@ -16,27 +16,27 @@ export function reconcileSettings(repo: Json, user: Json): Json {
16
16
  * `unique` — i.e. codepoint-sorted and deduplicated, matching jq).
17
17
  */
18
18
  export function mergeSettings(repo: Json, user: Json): Json {
19
- const candidate = deepMerge(user, repo)
20
- if (!isObject(candidate)) return candidate
21
- const merged = { ...candidate }
22
- const permissions = isObject(merged["permissions"]) ? merged["permissions"] : {}
19
+ const candidate = deepMerge(user, repo);
20
+ if (!isObject(candidate)) return candidate;
21
+ const merged = { ...candidate };
22
+ const permissions = isObject(merged["permissions"]) ? merged["permissions"] : {};
23
23
  merged["permissions"] = {
24
24
  ...permissions,
25
25
  allow: unionPermissions(user, repo, "allow"),
26
26
  deny: unionPermissions(user, repo, "deny"),
27
- ask: unionPermissions(user, repo, "ask")
28
- }
29
- return merged
27
+ ask: unionPermissions(user, repo, "ask"),
28
+ };
29
+ return merged;
30
30
  }
31
31
 
32
32
  function unionPermissions(user: Json, repo: Json, key: string): Array<string> {
33
- return uniqueStrings([...permissionArray(user, key), ...permissionArray(repo, key)])
33
+ return uniqueStrings([...permissionArray(user, key), ...permissionArray(repo, key)]);
34
34
  }
35
35
 
36
36
  function permissionArray(doc: Json, key: string): Array<string> {
37
- if (!isObject(doc)) return []
38
- const permissions = doc["permissions"]
39
- if (!isObject(permissions)) return []
40
- const arr = permissions[key]
41
- return Array.isArray(arr) ? arr.filter((x): x is string => typeof x === "string") : []
37
+ if (!isObject(doc)) return [];
38
+ const permissions = doc["permissions"];
39
+ if (!isObject(permissions)) return [];
40
+ const arr = permissions[key];
41
+ return Array.isArray(arr) ? arr.filter((x): x is string => typeof x === "string") : [];
42
42
  }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * EngineNative shared shapes: each cross-cutting data shape is defined once
3
+ * here so pipelines import one canonical definition instead of redeclaring
4
+ * structurally identical anonymous types. Types only — no runtime code.
5
+ */
6
+ import type { Json } from "./jq";
7
+ import type { ModifierFlag } from "./engineCtx";
8
+
9
+ /** Manifest-record object: the `{ [key]: Json }` shape redeclared across manifest readers. */
10
+ export type JsonObject = { [key: string]: Json };
11
+
12
+ /**
13
+ * Shared core of per-tool settings-edit descriptors (Claude JSON modifiers,
14
+ * Codex TOML passes). Each tool narrows `key` to its own vocabulary and keeps
15
+ * tool-only fields (such as Claude's dry-run preview) on its local extension.
16
+ */
17
+ export interface SettingEdit<TKey extends string> {
18
+ readonly tag: string;
19
+ readonly key: TKey;
20
+ readonly value: string | undefined;
21
+ readonly changed: string;
22
+ readonly unchanged: string;
23
+ }
24
+
25
+ /**
26
+ * omp session model (moved from harnesses.ts, which re-exports it so existing
27
+ * import paths keep working).
28
+ */
29
+ export interface OmpSessionModel {
30
+ readonly selector: string;
31
+ // Session ceiling; absent when the model publishes no ladder, so no
32
+ // invented level ever reaches a selector that omp must resolve.
33
+ readonly thinking?: string;
34
+ // Advisor level; absent when the model publishes no ladder.
35
+ readonly advisorThinking?: string;
36
+ }
37
+
38
+ /**
39
+ * Scalar modifier flags: the ModifierFlag subset taking a value through
40
+ * setModifier. Derived by exclusion so a new ModifierFlag forces an explicit
41
+ * scalar/non-scalar decision here.
42
+ */
43
+ export type ScalarModifierFlag = Exclude<
44
+ ModifierFlag,
45
+ "--claude-compact-window" | "--claude-permissive" | "--claude-plugin"
46
+ >;
@@ -0,0 +1,105 @@
1
+ /**
2
+ * EngineNative universal-skill install pass (`npx skills@<pin> add`) with
3
+ * Claude symlink healing. Split from skillsSync.ts; the orchestration still
4
+ * lives there. The prune pass (skillsPrune.ts) shares `skillsCli`.
5
+ */
6
+ import { p, spawnProcess } from "./exec";
7
+ import type { Ctx } from "./index";
8
+ import { ExitError } from "./parseArgs";
9
+ import { healClaudeSymlink, isDir } from "./skillsLinks";
10
+ import { normalizeManifest } from "./skillsManifest";
11
+ import { field } from "./toolchain";
12
+
13
+ export interface SkillsState {
14
+ present: number;
15
+ }
16
+
17
+ /** skills::_skills_cli — the pinned npx package spec. */
18
+ export function skillsCli(ctx: Ctx): string {
19
+ const version = field("skills-cli", "verified");
20
+ if (version !== "") return `skills@${version}`;
21
+ ctx.services.logger.err(
22
+ "Universal skills sync aborted because SoT/toolchain.json has no verified skills-cli pin",
23
+ );
24
+ throw new ExitError(1);
25
+ }
26
+
27
+ export async function syncUniversal(
28
+ ctx: Ctx,
29
+ state: SkillsState,
30
+ skillsDir: string,
31
+ manifest: string,
32
+ ): Promise<void> {
33
+ const { change, clearProgress, echo, progress, verbose, warn } = ctx.services.logger;
34
+ if (ctx.services.deps.probe("npx").state === "missing") {
35
+ ctx.services.deps.warnMissing(
36
+ "npx",
37
+ ctx.services.logger,
38
+ "skipping universal skills bootstrap",
39
+ );
40
+ return;
41
+ }
42
+
43
+ let added = 0;
44
+ let already = 0;
45
+ let failed = 0;
46
+ let healed = 0;
47
+
48
+ for (const slug of normalizeManifest(manifest)) {
49
+ const base = slug.slice(slug.lastIndexOf("/") + 1);
50
+
51
+ if (ctx.dryRun) {
52
+ if (isDir(p(skillsDir, base))) {
53
+ echo(`[dry-run] universal skill present: ${base}`);
54
+ healClaudeSymlink(ctx, skillsDir, base);
55
+ } else {
56
+ echo(`[dry-run] npx ${skillsCli(ctx)} add ${slug} -g -y -a claude-code codex`);
57
+ }
58
+ continue;
59
+ }
60
+
61
+ if (isDir(p(skillsDir, base))) {
62
+ already++;
63
+ if (healClaudeSymlink(ctx, skillsDir, base)) healed++;
64
+ continue;
65
+ }
66
+
67
+ progress(`Installing universal skill ${slug}...`);
68
+ const res = await spawnProcess(
69
+ "npx",
70
+ ["--yes", skillsCli(ctx), "add", slug, "-g", "-y", "-a", "claude-code", "codex"],
71
+ {
72
+ stdio: "ignore",
73
+ },
74
+ );
75
+ clearProgress();
76
+ if (res.error === undefined && res.exitCode === 0) {
77
+ added++;
78
+ } else {
79
+ warn(`Failed to install universal skill: ${slug}`);
80
+ failed++;
81
+ }
82
+ }
83
+
84
+ if (ctx.dryRun) return;
85
+
86
+ state.present = added + already;
87
+
88
+ if (added > 0) {
89
+ change(`Universal skills synced (+${added} new, ${already} already present)`);
90
+ ctx.nextStepTriggers.skillsRestart = true;
91
+ } else {
92
+ verbose(`Universal skills already in sync (${already} present)`);
93
+ }
94
+ if (healed > 0) {
95
+ change(
96
+ `Claude per-tool symlinks healed (+${healed}) — canonical present, ~/.claude/skills/<name> was missing or broken`,
97
+ );
98
+ ctx.nextStepTriggers.skillsRestart = true;
99
+ }
100
+ if (failed > 0) {
101
+ warn(
102
+ `${failed} skill install(s) failed — re-run sync or install manually with: npx skills add <slug> -g -y -a claude-code codex`,
103
+ );
104
+ }
105
+ }