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
@@ -1,41 +1,43 @@
1
- import { Argument, Command, Flag, Prompt } from "effect/unstable/cli"
2
- import { Console, Effect, Option } from "effect"
3
- import { chmodSync, mkdirSync, writeFileSync } from "node:fs"
4
- import { bail } from "../engine"
5
- import { capture, p, spawnHost, which } from "../engine-native/exec"
1
+ import { Argument, Command, Flag, Prompt } from "effect/unstable/cli";
2
+ import { Console, Effect, Option } from "effect";
3
+ import { chmodSync, mkdirSync, writeFileSync } from "node:fs";
4
+ import { bail } from "../engine";
5
+ import { capture, p, spawnHost, which } from "../engine-native/exec";
6
6
  import {
7
7
  DEFAULT_OMP_SESSION_MODEL,
8
8
  engineHome,
9
9
  readOmpSessionModel,
10
10
  writeOmpSessionModel,
11
- type OmpSessionModel
12
- } from "../engine-native/harnesses"
11
+ type OmpSessionModel,
12
+ } from "../engine-native/harnesses";
13
13
  import {
14
14
  advisorLevelFor,
15
+ advisorRecommendation,
15
16
  buildOmpArgs,
16
17
  ladderCeiling,
17
18
  overlayFileName,
19
+ planEffortChoice,
18
20
  parseFreeModels,
19
21
  renderFreeOverlay,
20
- type CatalogModel
21
- } from "../engine-native/ompOverlay"
22
+ type CatalogModel,
23
+ } from "../engine-native/ompOverlay";
22
24
 
23
25
  const model = Flag.String("model").pipe(
24
26
  Flag.withDescription("Free model selector to use and remember for omp sessions"),
25
- Flag.optional
26
- )
27
+ Flag.optional,
28
+ );
27
29
  const pick = Flag.Boolean("pick").pipe(
28
30
  Flag.withDescription("Choose the free model interactively"),
29
- Flag.withDefault(false)
30
- )
31
+ Flag.withDefault(false),
32
+ );
31
33
  const args: Argument.Argument<ReadonlyArray<string>> = Argument.String("args").pipe(
32
34
  Argument.withDescription("Arguments forwarded verbatim to omp"),
33
- Argument.variadic()
34
- )
35
+ Argument.variadic(),
36
+ );
35
37
 
36
38
  export type FreeSelectorResolution =
37
39
  | { readonly ok: true; readonly model: CatalogModel }
38
- | { readonly ok: false; readonly message: string }
40
+ | { readonly ok: false; readonly message: string };
39
41
 
40
42
  // Pure selector matching so the picker rule stays testable without spawning omp.
41
43
  // An exact selector always wins; otherwise one case-insensitive substring hit
@@ -43,147 +45,235 @@ export type FreeSelectorResolution =
43
45
  // to mistype and the catalog is small enough to disambiguate safely.
44
46
  export function resolveFreeSelector(
45
47
  input: string,
46
- free: ReadonlyArray<CatalogModel>
48
+ free: ReadonlyArray<CatalogModel>,
47
49
  ): FreeSelectorResolution {
48
- const trimmed = input.trim()
50
+ const trimmed = input.trim();
49
51
  if (trimmed === "") {
50
- return { ok: false, message: "Model selector must not be empty or blank" }
52
+ return { ok: false, message: "Model selector must not be empty or blank" };
51
53
  }
52
- const exact = free.find((candidate) => candidate.selector === trimmed)
53
- if (exact !== undefined) return { ok: true, model: exact }
54
- const lowered = trimmed.toLowerCase()
55
- const matches = free.filter((candidate) => candidate.selector.toLowerCase().includes(lowered))
56
- const list = free.map((candidate) => candidate.selector).join(", ")
54
+ const exact = free.find((candidate) => candidate.selector === trimmed);
55
+ if (exact !== undefined) return { ok: true, model: exact };
56
+ const lowered = trimmed.toLowerCase();
57
+ const matches = free.filter((candidate) => candidate.selector.toLowerCase().includes(lowered));
58
+ const list = free.map((candidate) => candidate.selector).join(", ");
57
59
  if (matches.length === 0) {
58
60
  return {
59
61
  ok: false,
60
- message: `Model '${trimmed}' is not in the free catalog. Free models: ${list}`
61
- }
62
+ message: `Model '${trimmed}' is not in the free catalog. Free models: ${list}`,
63
+ };
62
64
  }
63
- const only = matches[0]
64
- if (matches.length === 1 && only !== undefined) return { ok: true, model: only }
65
+ const only = matches[0];
66
+ if (matches.length === 1 && only !== undefined) return { ok: true, model: only };
65
67
  return {
66
68
  ok: false,
67
- message: `Model '${trimmed}' is ambiguous; matches: ${matches.map((candidate) => candidate.selector).join(", ")}. Pass the exact selector.`
68
- }
69
+ message: `Model '${trimmed}' is ambiguous; matches: ${matches.map((candidate) => candidate.selector).join(", ")}. Pass the exact selector.`,
70
+ };
69
71
  }
70
72
 
71
73
  const loadFreeCatalog = (omp: string): Effect.Effect<ReadonlyArray<CatalogModel>> =>
72
74
  Effect.gen(function* () {
73
- const raw = yield* Effect.promise(() => capture(omp, ["models", "--json"]))
75
+ const raw = yield* Effect.promise(() => capture(omp, ["models", "--json"]));
74
76
  if (raw === "") {
75
77
  return yield* bail(
76
- "'omp models --json' returned no output; verify omp runs on this host, then retry"
77
- )
78
+ "'omp models --json' returned no output; verify omp runs on this host, then retry",
79
+ );
78
80
  }
79
- let parsed: unknown
81
+ let parsed: unknown;
80
82
  try {
81
- parsed = JSON.parse(raw) as unknown
83
+ parsed = JSON.parse(raw) as unknown;
82
84
  } catch {
83
85
  return yield* bail(
84
- "'omp models --json' returned output that is not JSON; verify the omp version, then retry"
85
- )
86
+ "'omp models --json' returned output that is not JSON; verify the omp version, then retry",
87
+ );
86
88
  }
87
- const free = parseFreeModels(parsed)
89
+ const free = parseFreeModels(parsed);
88
90
  if (free.length === 0) {
89
- return yield* bail("'omp models --json' listed no free models; verify the omp login, then retry")
91
+ return yield* bail(
92
+ "'omp models --json' listed no free models; verify the omp login, then retry",
93
+ );
94
+ }
95
+ return free;
96
+ });
97
+
98
+ /**
99
+ * Ask for the thinking levels of one model. Every choice comes from that
100
+ * model own ladder, because omp rejects a `:level` suffix the model does not
101
+ * publish. A model with no ladder and a model with one level are settled
102
+ * without a question.
103
+ */
104
+ const pickLevels = (catalogModel: CatalogModel) =>
105
+ Effect.gen(function* () {
106
+ const plan = planEffortChoice(catalogModel.thinking);
107
+ if (plan.kind === "none") {
108
+ yield* Console.error(
109
+ `${catalogModel.name} publishes no thinking levels; the model default applies`,
110
+ );
111
+ return { thinking: undefined, advisorThinking: undefined };
90
112
  }
91
- return free
92
- })
113
+ if (plan.kind === "fixed") {
114
+ yield* Console.error(`${catalogModel.name} publishes one thinking level: ${plan.level}`);
115
+ return { thinking: plan.level, advisorThinking: plan.level };
116
+ }
117
+ const uniform = yield* Prompt.Select({
118
+ message: "Use the same thinking level for every role?",
119
+ choices: [
120
+ {
121
+ title: "Yes",
122
+ value: true,
123
+ description: `every role runs at ${plan.highest}, the highest this model offers`,
124
+ },
125
+ { title: "No", value: false, description: "set the advisor apart from the other roles" },
126
+ ],
127
+ });
128
+ if (uniform) return { thinking: plan.highest, advisorThinking: plan.highest };
129
+ const ladderChoices = plan.levels.map((level) => ({
130
+ title: level,
131
+ value: level,
132
+ description:
133
+ level === plan.highest
134
+ ? "highest this model offers"
135
+ : level === plan.levels[0]
136
+ ? "lowest this model offers"
137
+ : "",
138
+ }));
139
+ const thinking = yield* Prompt.Select({
140
+ message: "Thinking level for all roles except the advisor",
141
+ choices: ladderChoices,
142
+ });
143
+ const suggested = advisorRecommendation(plan.levels, thinking);
144
+ // The list leads with the suggestion, because Prompt.Select always starts
145
+ // on the first entry and offers no initial index. The full ladder follows
146
+ // in ladder order, so a different level stays one keypress away. A
147
+ // suggestion equal to the main level would only duplicate a row.
148
+ const advisorChoices =
149
+ suggested === undefined || suggested === thinking
150
+ ? ladderChoices
151
+ : [
152
+ {
153
+ title: suggested,
154
+ value: suggested,
155
+ description: `recommended, two levels below ${thinking}`,
156
+ },
157
+ ...ladderChoices,
158
+ ];
159
+ const advisorThinking = yield* Prompt.Select({
160
+ message: "Thinking level for the advisor role",
161
+ choices: advisorChoices,
162
+ });
163
+ return { thinking, advisorThinking };
164
+ });
93
165
 
94
166
  export const ompCommand = Command.make("omp", { model, pick, args }, (config) =>
95
167
  Effect.gen(function* () {
96
168
  if (Option.isSome(config.model) && config.pick) {
97
- return yield* bail("Pass either --model <selector> or --pick, not both")
169
+ return yield* bail("Pass either --model <selector> or --pick, not both");
98
170
  }
99
171
 
100
- const omp = yield* Effect.sync(() => which("omp"))
172
+ const omp = yield* Effect.sync(() => which("omp"));
101
173
  if (omp === "") {
102
- return yield* bail("omp not found on PATH; install omp, then retry", 1)
174
+ return yield* bail("omp not found on PATH; install omp, then retry", 1);
103
175
  }
104
176
 
105
- const home = engineHome(process.env)
177
+ const home = engineHome(process.env);
106
178
  if (Option.isSome(config.model)) {
107
- const free = yield* loadFreeCatalog(omp)
108
- const resolved = resolveFreeSelector(config.model.value, free)
109
- if (!resolved.ok) return yield* bail(resolved.message)
179
+ const free = yield* loadFreeCatalog(omp);
180
+ const resolved = resolveFreeSelector(config.model.value, free);
181
+ if (!resolved.ok) return yield* bail(resolved.message);
110
182
  // Compute both levels once from the catalog row so a plain launch
111
183
  // never needs the catalog again.
112
- const thinking = ladderCeiling(resolved.model.thinking)
113
- const advisorThinking = advisorLevelFor(resolved.model.thinking)
184
+ const thinking = ladderCeiling(resolved.model.thinking);
185
+ const advisorThinking = advisorLevelFor(resolved.model.thinking);
114
186
  yield* Effect.sync(() =>
115
- writeOmpSessionModel(home, { selector: resolved.model.selector, thinking, advisorThinking })
116
- )
187
+ writeOmpSessionModel(home, {
188
+ selector: resolved.model.selector,
189
+ thinking,
190
+ advisorThinking,
191
+ }),
192
+ );
117
193
  }
118
194
 
119
195
  if (config.pick) {
120
196
  // Check the terminal before the catalog spawn, so a non-interactive run
121
197
  // fails with the actionable message instead of an omp subprocess first.
122
198
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
123
- return yield* bail("Picking needs a terminal; pass --model <selector> instead")
199
+ return yield* bail("Picking needs a terminal; pass --model <selector> instead");
124
200
  }
125
- const free = yield* loadFreeCatalog(omp)
201
+ const free = yield* loadFreeCatalog(omp);
126
202
  const chosen = yield* Prompt.Select({
127
203
  message: "Choose the free model for this omp session",
128
204
  choices: free.map((candidate) => {
129
- const ceiling = ladderCeiling(candidate.thinking)
205
+ const ceiling = ladderCeiling(candidate.thinking);
130
206
  return {
131
207
  title: `${candidate.name} — ${candidate.selector}`,
132
208
  value: candidate.selector,
133
209
  description:
134
210
  ceiling === undefined
135
211
  ? `model default thinking; ${candidate.contextWindow} context`
136
- : `thinking to ${ceiling}; ${candidate.contextWindow} context`
137
- }
138
- })
139
- })
140
- const match = free.find((candidate) => candidate.selector === chosen)
141
- if (match === undefined) return yield* bail(`Model '${chosen}' is not in the free catalog`)
142
- const thinking = ladderCeiling(match.thinking)
143
- const advisorThinking = advisorLevelFor(match.thinking)
144
- yield* Effect.sync(() => writeOmpSessionModel(home, { selector: match.selector, thinking, advisorThinking }))
212
+ : `thinking to ${ceiling}; ${candidate.contextWindow} context`,
213
+ };
214
+ }),
215
+ });
216
+ const match = free.find((candidate) => candidate.selector === chosen);
217
+ if (match === undefined) return yield* bail(`Model '${chosen}' is not in the free catalog`);
218
+ const chosenLevels = yield* pickLevels(match);
219
+ yield* Effect.sync(() =>
220
+ writeOmpSessionModel(home, {
221
+ selector: match.selector,
222
+ thinking: chosenLevels.thinking,
223
+ advisorThinking: chosenLevels.advisorThinking,
224
+ }),
225
+ );
145
226
  }
146
227
 
147
- const session: OmpSessionModel = (yield* Effect.sync(() => readOmpSessionModel(home))) ??
148
- DEFAULT_OMP_SESSION_MODEL
228
+ const session: OmpSessionModel =
229
+ (yield* Effect.sync(() => readOmpSessionModel(home))) ?? DEFAULT_OMP_SESSION_MODEL;
149
230
 
150
231
  // A stable cache path, not a temp file deleted at exit, because omp may
151
232
  // re-read the overlay during a live session. The name carries the model,
152
233
  // so a second launcher on another model cannot rewrite the file a running
153
234
  // session still reads.
154
- const directory = p(home, ".cache", "docks-kit")
155
- const overlayPath = p(directory, overlayFileName(session.selector))
235
+ const directory = p(home, ".cache", "docks-kit");
236
+ const overlayPath = p(directory, overlayFileName(session.selector));
156
237
  yield* Effect.sync(() => {
157
238
  // `mode` applies only when mkdir creates the path, so an existing
158
239
  // permissive cache directory keeps its mode until the chmod below.
159
- mkdirSync(directory, { recursive: true, mode: 0o700 })
160
- chmodSync(directory, 0o700)
161
- writeFileSync(overlayPath, renderFreeOverlay(session), { mode: 0o600 })
162
- chmodSync(overlayPath, 0o600)
163
- })
240
+ mkdirSync(directory, { recursive: true, mode: 0o700 });
241
+ chmodSync(directory, 0o700);
242
+ writeFileSync(overlayPath, renderFreeOverlay(session), { mode: 0o600 });
243
+ chmodSync(overlayPath, 0o600);
244
+ });
164
245
 
165
246
  // A level-free model states the model default so the line never shows
166
- // an undefined level.
167
- const hasLevel = typeof session.thinking === "string" && session.thinking.trim() !== ""
247
+ // an undefined level. The advisor appears only when it differs, because
248
+ // a uniform session has nothing extra to report.
249
+ const level =
250
+ typeof session.thinking === "string" && session.thinking.trim() !== ""
251
+ ? session.thinking
252
+ : undefined;
253
+ const advisor =
254
+ typeof session.advisorThinking === "string" && session.advisorThinking.trim() !== ""
255
+ ? session.advisorThinking
256
+ : undefined;
257
+ const levelText =
258
+ level === undefined ? "the model default thinking level" : `${level} thinking`;
259
+ const advisorText = advisor !== undefined && advisor !== level ? `, advisor at ${advisor}` : "";
168
260
  yield* Console.error(
169
- hasLevel
170
- ? `Starting omp with ${session.selector} at ${session.thinking} thinking (session only; deployed config unchanged)`
171
- : `Starting omp with ${session.selector} at the model default thinking level (session only; deployed config unchanged)`
172
- )
261
+ `Starting omp with ${session.selector} at ${levelText}${advisorText} (session only; deployed config unchanged)`,
262
+ );
173
263
 
174
264
  const child = yield* Effect.sync(() =>
175
- spawnHost("omp", buildOmpArgs(overlayPath, config.args), { stdio: "inherit" })
176
- )
265
+ spawnHost("omp", buildOmpArgs(overlayPath, config.args), { stdio: "inherit" }),
266
+ );
177
267
  if (child.error !== undefined) {
178
268
  return yield* bail(
179
- `Failed to start omp: ${child.error instanceof Error ? child.error.message : String(child.error)}`
180
- )
269
+ `Failed to start omp: ${child.error instanceof Error ? child.error.message : String(child.error)}`,
270
+ );
181
271
  }
182
272
  // A signalled child reports a null status, which must not look successful.
183
- yield* Effect.sync(() => process.exit(child.status ?? 1))
184
- })
273
+ yield* Effect.sync(() => process.exit(child.status ?? 1));
274
+ }),
185
275
  ).pipe(
186
276
  Command.withDescription(
187
- "Start omp with every model role overridden to the remembered free model for this run only (no deployed configuration is changed; --model or --pick remembers a new free default)."
188
- )
189
- )
277
+ "Start omp with every model role overridden to the remembered free model for this run only (no deployed configuration is changed; --model or --pick remembers a new free default).",
278
+ ),
279
+ );
@@ -1,35 +1,35 @@
1
- import { Argument, Command, Flag } from "effect/unstable/cli"
2
- import { Console, Effect, Option } from "effect"
3
- import { bail } from "../engine"
4
- import { pluginsView } from "../manifests"
1
+ import { Argument, Command, Flag } from "effect/unstable/cli";
2
+ import { Console, Effect, Option } from "effect";
3
+ import { bail } from "../engine";
4
+ import { pluginsView } from "../manifests";
5
5
 
6
6
  const action = Argument.String("action").pipe(
7
7
  Argument.withDescription("list (default)"),
8
- Argument.optional
9
- )
8
+ Argument.optional,
9
+ );
10
10
  const json = Flag.Boolean("json").pipe(
11
11
  Flag.withDescription("Machine-readable output"),
12
- Flag.withDefault(false)
13
- )
12
+ Flag.withDefault(false),
13
+ );
14
14
 
15
15
  export const pluginsCommand = Command.make("plugins", { action, json }, (config) =>
16
16
  Effect.gen(function* () {
17
- const act = Option.getOrElse(config.action, () => "list")
17
+ const act = Option.getOrElse(config.action, () => "list");
18
18
  if (act !== "list") {
19
19
  return yield* bail(
20
- `Unknown plugins action '${act}' (valid: list). Install/removal runs through sync: --claude-plugin=<name> opts in, --prune reconciles.`
21
- )
20
+ `Unknown plugins action '${act}' (valid: list). Install/removal runs through sync: --claude-plugin=<name> opts in, --prune reconciles.`,
21
+ );
22
22
  }
23
- const view = pluginsView()
23
+ const view = pluginsView();
24
24
  if (config.json) {
25
- return yield* Console.log(JSON.stringify(view, null, 2))
25
+ return yield* Console.log(JSON.stringify(view, null, 2));
26
26
  }
27
- yield* Console.log("SoT tri-state: true = enabled everywhere, false = installed-but-disabled (per-project enable), absent = not kit-managed\n")
28
- yield* Console.log(`${"PLUGIN".padEnd(44)} ${"SOT".padEnd(7)} INSTALLED`)
27
+ yield* Console.log(
28
+ "SoT tri-state: true = enabled everywhere, false = installed-but-disabled (per-project enable), absent = not kit-managed\n",
29
+ );
30
+ yield* Console.log(`${"PLUGIN".padEnd(44)} ${"SOT".padEnd(7)} INSTALLED`);
29
31
  for (const p of view) {
30
- yield* Console.log(`${p.plugin.padEnd(44)} ${p.sot.padEnd(7)} ${p.installed ? "yes" : "no"}`)
32
+ yield* Console.log(`${p.plugin.padEnd(44)} ${p.sot.padEnd(7)} ${p.installed ? "yes" : "no"}`);
31
33
  }
32
- })
33
- ).pipe(
34
- Command.withDescription("Installed plugins vs SoT enabledPlugins tri-state (read-only).")
35
- )
34
+ }),
35
+ ).pipe(Command.withDescription("Installed plugins vs SoT enabledPlugins tri-state (read-only)."));
@@ -1,38 +1,40 @@
1
- import { Argument, Command, Flag } from "effect/unstable/cli"
2
- import { Console, Effect, Option } from "effect"
3
- import { bail } from "../engine"
4
- import { skillsView } from "../manifests"
1
+ import { Argument, Command, Flag } from "effect/unstable/cli";
2
+ import { Console, Effect, Option } from "effect";
3
+ import { bail } from "../engine";
4
+ import { skillsView } from "../manifests";
5
5
 
6
6
  const action = Argument.String("action").pipe(
7
7
  Argument.withDescription("list (default)"),
8
- Argument.optional
9
- )
8
+ Argument.optional,
9
+ );
10
10
  const json = Flag.Boolean("json").pipe(
11
11
  Flag.withDescription("Machine-readable output"),
12
- Flag.withDefault(false)
13
- )
12
+ Flag.withDefault(false),
13
+ );
14
14
 
15
15
  export const skillsCommand = Command.make("skills", { action, json }, (config) =>
16
16
  Effect.gen(function* () {
17
- const act = Option.getOrElse(config.action, () => "list")
17
+ const act = Option.getOrElse(config.action, () => "list");
18
18
  if (act !== "list") {
19
19
  return yield* bail(
20
- `Unknown skills action '${act}' (valid: list). Declare skills in SoT/.agents/skills.txt and run sync; --prune removes undeclared kit-managed ones.`
21
- )
20
+ `Unknown skills action '${act}' (valid: list). Declare skills in SoT/.agents/skills.txt and run sync; --prune removes undeclared kit-managed ones.`,
21
+ );
22
22
  }
23
- const view = skillsView()
23
+ const view = skillsView();
24
24
  if (config.json) {
25
- return yield* Console.log(JSON.stringify(view, null, 2))
25
+ return yield* Console.log(JSON.stringify(view, null, 2));
26
26
  }
27
- yield* Console.log(`${"SKILL".padEnd(28)} ${"DECLARED".padEnd(9)} ${"INSTALLED".padEnd(10)} CLAUDE-SYMLINK`)
27
+ yield* Console.log(
28
+ `${"SKILL".padEnd(28)} ${"DECLARED".padEnd(9)} ${"INSTALLED".padEnd(10)} CLAUDE-SYMLINK`,
29
+ );
28
30
  for (const s of view) {
29
31
  yield* Console.log(
30
- `${s.skill.padEnd(28)} ${(s.declared ? "yes" : "no").padEnd(9)} ${(s.installed ? "yes" : "no").padEnd(10)} ${s.claudeSymlink ? "ok" : "-"}`
31
- )
32
+ `${s.skill.padEnd(28)} ${(s.declared ? "yes" : "no").padEnd(9)} ${(s.installed ? "yes" : "no").padEnd(10)} ${s.claudeSymlink ? "ok" : "-"}`,
33
+ );
32
34
  }
33
- })
35
+ }),
34
36
  ).pipe(
35
37
  Command.withDescription(
36
- "Universal agent skills: SoT/.agents/skills.txt vs ~/.agents/skills + the ~/.claude/skills symlink health (read-only)."
37
- )
38
- )
38
+ "Universal agent skills: SoT/.agents/skills.txt vs ~/.agents/skills + the ~/.claude/skills symlink health (read-only).",
39
+ ),
40
+ );