@klhapp/skillmux 1.10.0 → 1.11.1

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.
@@ -1,8 +1,8 @@
1
1
  import { expandHome, migrateLegacyPaths, resolveConfigPath } from "../config";
2
- import { type TargetAdapter } from "../adapters";
2
+ import { type ContextAdapter } from "../adapters";
3
3
  import { type ResolvedContext } from "../context";
4
4
  import { applyConfigInit, planConfigInit, type ConfigInitPlan } from "../setup";
5
- import { emitSuccess, isInteractive, renderTargetBanner } from "../output";
5
+ import { emitSuccess, isInteractive, renderContextBanner, unknownSubcommandError } from "../output";
6
6
  import { confirmAction } from "./shared";
7
7
  import { isGlobalFlag } from "../global-flags";
8
8
  function emitConfigInitOutcome(
@@ -38,10 +38,10 @@ function emitConfigInitOutcome(
38
38
  }
39
39
 
40
40
  export async function handleConfigCommand(
41
- adapter: TargetAdapter,
41
+ adapter: ContextAdapter,
42
42
  sub: string,
43
43
  args: string[],
44
- ctx: { target: ResolvedContext; isJson: boolean; dryRun: boolean },
44
+ ctx: { context: ResolvedContext; isJson: boolean; dryRun: boolean },
45
45
  ) {
46
46
  if (sub === "init") {
47
47
  let vaultPath: string | undefined;
@@ -127,8 +127,8 @@ export async function handleConfigCommand(
127
127
  if (sub === "show") {
128
128
  const withSources = args.includes("--sources");
129
129
  const data = await adapter.getConfigShow();
130
- emitSuccess({ isJson: ctx.isJson, target: ctx.target }, data, () => {
131
- renderTargetBanner(ctx.target);
130
+ emitSuccess({ isJson: ctx.isJson, context: ctx.context }, data, () => {
131
+ renderContextBanner(ctx.context);
132
132
  if (withSources) {
133
133
  const policy =
134
134
  data.effective.config?.environment_overrides === false
@@ -151,7 +151,7 @@ export async function handleConfigCommand(
151
151
  if (!key) throw new Error("usage: skillmux config get <key>");
152
152
  const val = await adapter.getConfigGet(key);
153
153
  emitSuccess(
154
- { isJson: ctx.isJson, target: ctx.target },
154
+ { isJson: ctx.isJson, context: ctx.context },
155
155
  { key, value: val },
156
156
  () => {
157
157
  console.log(
@@ -164,7 +164,7 @@ export async function handleConfigCommand(
164
164
 
165
165
  if (sub === "validate") {
166
166
  const res = await adapter.configValidate();
167
- emitSuccess({ isJson: ctx.isJson, target: ctx.target }, res, () => {
167
+ emitSuccess({ isJson: ctx.isJson, context: ctx.context }, res, () => {
168
168
  console.log(res.valid ? "configuration is valid" : "configuration is invalid");
169
169
  });
170
170
  return;
@@ -172,8 +172,8 @@ export async function handleConfigCommand(
172
172
 
173
173
  if (sub === "diff") {
174
174
  const res = await adapter.configDiff();
175
- emitSuccess({ isJson: ctx.isJson, target: ctx.target }, res, () => {
176
- renderTargetBanner(ctx.target);
175
+ emitSuccess({ isJson: ctx.isJson, context: ctx.context }, res, () => {
176
+ renderContextBanner(ctx.context);
177
177
  console.log(JSON.stringify(res.diff, null, 2));
178
178
  });
179
179
  return;
@@ -186,8 +186,8 @@ export async function handleConfigCommand(
186
186
  throw new Error("usage: skillmux config set <key> <value> [--dry-run]");
187
187
  }
188
188
  const res = await adapter.configSet(key, value, { dryRun: ctx.dryRun });
189
- emitSuccess({ isJson: ctx.isJson, target: ctx.target }, res, () => {
190
- renderTargetBanner(ctx.target);
189
+ emitSuccess({ isJson: ctx.isJson, context: ctx.context }, res, () => {
190
+ renderContextBanner(ctx.context);
191
191
  const prefix = ctx.dryRun ? "[dry-run] " : "";
192
192
  console.log(
193
193
  `${prefix}${key}: ${JSON.stringify(res.prior_val)} -> ${JSON.stringify(res.resulting_val)}`,
@@ -201,8 +201,8 @@ export async function handleConfigCommand(
201
201
 
202
202
  if (sub === "status") {
203
203
  const res = await adapter.configStatus();
204
- emitSuccess({ isJson: ctx.isJson, target: ctx.target }, res, () => {
205
- renderTargetBanner(ctx.target);
204
+ emitSuccess({ isJson: ctx.isJson, context: ctx.context }, res, () => {
205
+ renderContextBanner(ctx.context);
206
206
  console.log(`runtime: ${res.runtime}`);
207
207
  console.log(`deployment runtime: ${res.deployment_runtime}`);
208
208
  console.log(`image variant: ${res.image_variant ?? "none"}`);
@@ -212,5 +212,13 @@ export async function handleConfigCommand(
212
212
  return;
213
213
  }
214
214
 
215
- throw new Error("usage: skillmux config show");
215
+ throw unknownSubcommandError("config", sub, [
216
+ "init",
217
+ "show",
218
+ "get",
219
+ "set",
220
+ "validate",
221
+ "diff",
222
+ "status",
223
+ ]);
216
224
  }
@@ -9,18 +9,19 @@ import {
9
9
  import {
10
10
  emitSuccess,
11
11
  renderTable,
12
- renderTargetBanner,
12
+ renderContextBanner,
13
+ unknownSubcommandError,
13
14
  } from "../output";
14
15
 
15
16
  export async function handleContextCommand(
16
17
  sub: string,
17
18
  args: string[],
18
- ctx: { target: ResolvedContext; isJson: boolean },
19
+ ctx: { context: ResolvedContext; isJson: boolean },
19
20
  ) {
20
21
  if (sub === "list") {
21
22
  const contexts = await listContexts();
22
- emitSuccess({ isJson: ctx.isJson, target: ctx.target }, contexts, () => {
23
- renderTargetBanner(ctx.target);
23
+ emitSuccess({ isJson: ctx.isJson, context: ctx.context }, contexts, () => {
24
+ renderContextBanner(ctx.context);
24
25
  renderTable(
25
26
  [
26
27
  { key: "name", header: "NAME" },
@@ -40,8 +41,8 @@ export async function handleContextCommand(
40
41
 
41
42
  if (sub === "current") {
42
43
  const current = await getCurrentContext();
43
- emitSuccess({ isJson: ctx.isJson, target: ctx.target }, current, () => {
44
- renderTargetBanner(ctx.target);
44
+ emitSuccess({ isJson: ctx.isJson, context: ctx.context }, current, () => {
45
+ renderContextBanner(ctx.context);
45
46
  console.log(`Current context: ${current.name} (${current.server})`);
46
47
  });
47
48
  return;
@@ -62,7 +63,7 @@ export async function handleContextCommand(
62
63
  }
63
64
  await addContext(name, { server, token_env: tokenEnv });
64
65
  emitSuccess(
65
- { isJson: ctx.isJson, target: ctx.target },
66
+ { isJson: ctx.isJson, context: ctx.context },
66
67
  { name, server, token_env: tokenEnv },
67
68
  () => {
68
69
  console.log(`Added context "${name}" -> ${server}`);
@@ -76,7 +77,7 @@ export async function handleContextCommand(
76
77
  if (!name) throw new Error("usage: skillmux context use <name>");
77
78
  await useContext(name);
78
79
  emitSuccess(
79
- { isJson: ctx.isJson, target: ctx.target },
80
+ { isJson: ctx.isJson, context: ctx.context },
80
81
  { default_context: name },
81
82
  () => {
82
83
  console.log(`Switched default context to "${name}"`);
@@ -90,7 +91,7 @@ export async function handleContextCommand(
90
91
  if (!name) throw new Error("usage: skillmux context remove <name>");
91
92
  await removeContext(name);
92
93
  emitSuccess(
93
- { isJson: ctx.isJson, target: ctx.target },
94
+ { isJson: ctx.isJson, context: ctx.context },
94
95
  { removed: name },
95
96
  () => {
96
97
  console.log(`Removed context "${name}"`);
@@ -99,5 +100,5 @@ export async function handleContextCommand(
99
100
  return;
100
101
  }
101
102
 
102
- throw new Error("usage: skillmux context <add|list|current|use|remove>");
103
+ throw unknownSubcommandError("context", sub, ["add", "list", "current", "use", "remove"]);
103
104
  }
@@ -1,6 +1,6 @@
1
1
  import { expandHome } from "../config";
2
2
  import { pinCore, unpinCore, validateManifest, writeManifestAtomic } from "../manifest";
3
- import { emitSuccess } from "../output";
3
+ import { emitSuccess, unknownSubcommandError } from "../output";
4
4
  import { confirmIfNeeded, loadManifestContext } from "./shared";
5
5
  export async function runCore(
6
6
  subCommand: string,
@@ -8,7 +8,7 @@ export async function runCore(
8
8
  options: { isJson: boolean; dryRun: boolean },
9
9
  ): Promise<void> {
10
10
  if (subCommand !== "pin" && subCommand !== "unpin") {
11
- throw new Error("usage: skillmux core <pin|unpin>");
11
+ throw unknownSubcommandError("core", subCommand, ["pin", "unpin"]);
12
12
  }
13
13
  const skillIds = args.filter((arg) => !arg.startsWith("-"));
14
14
  if (skillIds.length === 0) {
@@ -1,29 +1,50 @@
1
1
  import { resolveConfigPath } from "../config";
2
2
  import { diagnose } from "../doctor";
3
3
  import { getEffectiveConfig } from "../config-service";
4
- import type { TargetAdapter } from "../adapters";
4
+ import type { ContextAdapter } from "../adapters";
5
5
  import type { ResolvedContext } from "../context";
6
+ import { isGlobalFlag, isGlobalFlagWithValue } from "../global-flags";
6
7
  import {
7
8
  emitSuccess,
8
9
  green,
9
10
  red,
10
- renderTargetBanner,
11
+ renderContextBanner,
11
12
  } from "../output";
12
13
 
14
+ /**
15
+ * doctor takes no options of its own, but it still has to reject unknown ones
16
+ * rather than silently ignoring them the way every other command does.
17
+ */
18
+ export function parseDoctorArgs(args: readonly string[]): void {
19
+ for (let i = 0; i < args.length; i++) {
20
+ const option = args[i];
21
+ if (isGlobalFlag(option, "--json", "--allow-insecure", "--verbose")) {
22
+ // handled globally by main(); recognized here so it isn't rejected
23
+ } else if (isGlobalFlagWithValue(option)) {
24
+ // handled globally by main()'s resolveContext(); skip its value too
25
+ i++;
26
+ } else {
27
+ throw new Error(`unknown doctor option: ${option}`);
28
+ }
29
+ }
30
+ }
31
+
13
32
  export async function runDoctor(options: {
14
33
  isJson: boolean;
15
- target: ResolvedContext;
16
- adapter: TargetAdapter;
34
+ context: ResolvedContext;
35
+ adapter: ContextAdapter;
36
+ args?: readonly string[];
17
37
  }): Promise<void> {
18
- if (options.target.type === "remote") {
19
- const target = options.target;
38
+ parseDoctorArgs(options.args ?? []);
39
+ if (options.context.type === "remote") {
40
+ const context = options.context;
20
41
  const [status, caps] = await Promise.all([
21
42
  options.adapter.configStatus(),
22
43
  options.adapter.getCapabilities(),
23
44
  ]);
24
45
  const remoteReport = {
25
- target: target.name || target.server,
26
- server: target.server,
46
+ target: context.name || context.server,
47
+ server: context.server,
27
48
  version: status.version,
28
49
  deployment_runtime: status.deployment_runtime,
29
50
  image_variant: status.image_variant ?? null,
@@ -34,8 +55,8 @@ export async function runDoctor(options: {
34
55
  restart_required_keys: status.restart_required_keys,
35
56
  last_reload_error: status.last_reload_error,
36
57
  };
37
- emitSuccess({ isJson: options.isJson, target: options.target }, remoteReport, () => {
38
- renderTargetBanner(options.target);
58
+ emitSuccess({ isJson: options.isJson, target: options.context }, remoteReport, () => {
59
+ renderContextBanner(options.context);
39
60
  console.log(`server: ${remoteReport.server}`);
40
61
  console.log(`version: ${remoteReport.version}`);
41
62
  console.log(`deployment runtime: ${remoteReport.deployment_runtime}`);
@@ -5,20 +5,22 @@ import { excludeExistingCases, parseEvalCases } from "../eval";
5
5
  import { emitSuccess, warn } from "../output";
6
6
  import { parseSince } from "../stats";
7
7
  import { confirmIfNeeded } from "./shared";
8
- import type { TargetAdapter } from "../adapters";
8
+ import type { ContextAdapter } from "../adapters";
9
9
  import { isGlobalFlag, isGlobalFlagWithValue } from "../global-flags";
10
10
 
11
11
  export async function runEvalPromote(
12
12
  args: string[],
13
- options: { isJson: boolean; dryRun: boolean; adapter: TargetAdapter },
13
+ options: { isJson: boolean; dryRun: boolean; adapter: ContextAdapter },
14
14
  ): Promise<void> {
15
15
  let since: string | undefined;
16
+ let out: string | undefined;
16
17
  let target: string | undefined;
17
18
  let dryRun = options.dryRun;
18
19
  let yes = false;
19
20
  for (let i = 0; i < args.length; i++) {
20
21
  const arg = args[i];
21
22
  if (arg === "--since") since = args[++i];
23
+ else if (arg === "--out") out = args[++i];
22
24
  else if (arg === "--target") target = args[++i];
23
25
  else if (arg === "--dry-run") dryRun = true;
24
26
  else if (arg === "--yes") yes = true;
@@ -31,12 +33,20 @@ export async function runEvalPromote(
31
33
  }
32
34
  }
33
35
  if (!since) {
34
- throw new Error("usage: skillmux eval promote --since <window> [--target <path>] [--dry-run] [--yes] [--json]");
36
+ throw new Error("usage: skillmux eval promote --since <window> [--out <path>] [--dry-run] [--yes] [--json]");
37
+ }
38
+
39
+ if (out !== undefined && target !== undefined && out !== target) {
40
+ throw new Error("cannot specify conflicting --out and --target");
41
+ }
42
+
43
+ if (target !== undefined) {
44
+ warn("--target is deprecated, use --out instead");
35
45
  }
36
46
 
37
47
  const config = await loadConfig();
38
48
  const stateDir = expandHome(config.state_dir);
39
- const targetPath = target ?? join(stateDir, "eval-observed.json");
49
+ const targetPath = out ?? target ?? join(stateDir, "eval-observed.json");
40
50
  const sinceDate = parseSince(since);
41
51
  const sinceIso = sinceDate.toISOString();
42
52