@klhapp/skillmux 1.11.2 → 1.13.0

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 (104) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +20 -10
  3. package/bin/skillmux.js +89 -0
  4. package/docs/README.md +4 -4
  5. package/docs/cli.md +15 -5
  6. package/docs/concepts.md +4 -3
  7. package/docs/configuration.md +22 -5
  8. package/docs/deployment.md +3 -3
  9. package/docs/getting-started.md +32 -7
  10. package/docs/releasing.md +17 -2
  11. package/docs/sdd/cli-remote-target-parity/schema-bucket-b.yaml +297 -0
  12. package/docs/sdd/cli-remote-target-parity/spec-bucket-b.md +95 -0
  13. package/docs/sdd/cli-remote-target-parity/spec.md +111 -0
  14. package/docs/sdd/cli-remote-target-parity/think.md +183 -0
  15. package/docs/sdd/cli-surface-consistency/think.md +149 -0
  16. package/docs/sdd/config-authority-rerank-resilience/plan.md +338 -0
  17. package/docs/sdd/config-authority-rerank-resilience/review.md +124 -0
  18. package/docs/sdd/config-authority-rerank-resilience/spec.md +78 -0
  19. package/docs/sdd/feat-packaging-deployment/review.md +95 -0
  20. package/docs/sdd/fetch-outcome-flywheel/review.md +103 -0
  21. package/docs/sdd/fetch-outcome-flywheel/schema.json +347 -0
  22. package/docs/sdd/fetch-outcome-flywheel/spec.md +185 -0
  23. package/docs/sdd/npm-native-binaries/review.md +103 -0
  24. package/docs/sdd/npm-native-binaries/spec.md +148 -0
  25. package/docs/sdd/ranked-shortlist-2/pr3-calibration-removal.md +56 -0
  26. package/docs/sdd/refactor-builtin-target-paths/spec.md +41 -0
  27. package/docs/sdd/runtime-resource-hardening/review.md +86 -0
  28. package/docs/sdd/runtime-resource-hardening/spec.md +43 -0
  29. package/docs/sdd/security-hardening-phase/think.md +129 -0
  30. package/docs/sdd/security-observability/review.md +96 -0
  31. package/docs/sdd/security-observability/spec.md +55 -0
  32. package/docs/sdd/skill-provenance-update/review.md +152 -0
  33. package/docs/sdd/skill-provenance-update/schema.json +193 -0
  34. package/docs/sdd/skill-provenance-update/spec.md +134 -0
  35. package/docs/sdd/supply-chain-hardening/review.md +50 -0
  36. package/docs/sdd/supply-chain-hardening/spec.md +42 -0
  37. package/docs/sdd/target-marker-rehome/spec.md +33 -0
  38. package/docs/skill-management.md +1 -1
  39. package/docs/troubleshooting.md +18 -2
  40. package/package.json +15 -8
  41. package/src/adapters.ts +0 -438
  42. package/src/audit.ts +0 -21
  43. package/src/cli.ts +0 -928
  44. package/src/clients.ts +0 -368
  45. package/src/commands/audit.ts +0 -85
  46. package/src/commands/config.ts +0 -224
  47. package/src/commands/context.ts +0 -104
  48. package/src/commands/core.ts +0 -56
  49. package/src/commands/doctor.ts +0 -97
  50. package/src/commands/eval.ts +0 -88
  51. package/src/commands/init.ts +0 -672
  52. package/src/commands/install.ts +0 -155
  53. package/src/commands/local-vault.ts +0 -60
  54. package/src/commands/models.ts +0 -10
  55. package/src/commands/outdated.ts +0 -115
  56. package/src/commands/project.ts +0 -580
  57. package/src/commands/report.ts +0 -66
  58. package/src/commands/scan.ts +0 -71
  59. package/src/commands/shared.ts +0 -38
  60. package/src/commands/skill.ts +0 -33
  61. package/src/commands/sync.ts +0 -233
  62. package/src/commands/target.ts +0 -225
  63. package/src/commands/update.ts +0 -277
  64. package/src/completions.ts +0 -196
  65. package/src/concurrency-limiter.ts +0 -61
  66. package/src/config-service.ts +0 -398
  67. package/src/config-watcher.ts +0 -253
  68. package/src/config.ts +0 -583
  69. package/src/context.ts +0 -188
  70. package/src/db-audit.ts +0 -286
  71. package/src/db-index.ts +0 -238
  72. package/src/db.ts +0 -3
  73. package/src/deployment.ts +0 -39
  74. package/src/doctor.ts +0 -205
  75. package/src/eval.ts +0 -302
  76. package/src/global-flags.ts +0 -46
  77. package/src/init-agents.ts +0 -329
  78. package/src/init-instructions.ts +0 -192
  79. package/src/init.ts +0 -389
  80. package/src/install.ts +0 -232
  81. package/src/lifecycle.ts +0 -51
  82. package/src/logger.ts +0 -26
  83. package/src/manifest.ts +0 -352
  84. package/src/mcp-registration.ts +0 -89
  85. package/src/metrics.ts +0 -121
  86. package/src/models.ts +0 -20
  87. package/src/output.ts +0 -214
  88. package/src/project-setup.ts +0 -36
  89. package/src/prompts.ts +0 -124
  90. package/src/provenance.ts +0 -99
  91. package/src/rate-limiter.ts +0 -137
  92. package/src/readiness.ts +0 -30
  93. package/src/redact.ts +0 -52
  94. package/src/router-core.ts +0 -666
  95. package/src/rrf.ts +0 -31
  96. package/src/scan.ts +0 -300
  97. package/src/server.ts +0 -930
  98. package/src/setup.ts +0 -145
  99. package/src/snapshot.ts +0 -135
  100. package/src/stats.ts +0 -340
  101. package/src/sync.ts +0 -482
  102. package/src/toml-writer.ts +0 -51
  103. package/src/types.ts +0 -212
  104. package/src/vault.ts +0 -207
package/src/clients.ts DELETED
@@ -1,368 +0,0 @@
1
- import type { Clients, Config, RemoteRerankerConfig } from "./types";
2
- import { expandHome } from "./config";
3
- import { assertHostAllowed } from "./install";
4
- import type { pipeline as createPipeline } from "@huggingface/transformers";
5
-
6
- export type RemoteErrorKind = "configuration" | "availability" | "protocol";
7
-
8
- export class RemoteInferenceError extends Error {
9
- constructor(
10
- public readonly kind: RemoteErrorKind,
11
- message: string,
12
- ) {
13
- super(message);
14
- this.name = "RemoteInferenceError";
15
- }
16
- }
17
-
18
- // Lazy-loaded model instances for in-process ONNX inference
19
- type FeatureExtractor = Awaited<ReturnType<typeof createPipeline<"feature-extraction">>>;
20
-
21
- let localEmbedder: FeatureExtractor | null = null;
22
-
23
- function authorizationHeaders(
24
- apiKeyEnv: string | undefined,
25
- configKey: string,
26
- ): Record<string, string> {
27
- if (apiKeyEnv === undefined) return {};
28
- const apiKey = process.env[apiKeyEnv];
29
- if (apiKey === undefined || apiKey === "") {
30
- throw new RemoteInferenceError(
31
- "configuration",
32
- `${configKey} names environment variable "${apiKeyEnv}", but it is unset or empty.`,
33
- );
34
- }
35
- return { authorization: `Bearer ${apiKey}` };
36
- }
37
-
38
- function assertInferenceHostAllowed(url: string, allowedHosts: string[] | undefined): void {
39
- try {
40
- assertHostAllowed(url, allowedHosts);
41
- } catch (error) {
42
- throw new RemoteInferenceError(
43
- "configuration",
44
- error instanceof Error ? error.message : String(error),
45
- );
46
- }
47
- }
48
-
49
- function httpFailure(surface: string, status: number): RemoteInferenceError {
50
- const kind: RemoteErrorKind =
51
- status === 401 || status === 403
52
- ? "configuration"
53
- : status === 408 || status === 429 || status >= 500
54
- ? "availability"
55
- : "protocol";
56
- return new RemoteInferenceError(kind, `${surface} returned HTTP ${status}`);
57
- }
58
-
59
- function finiteFloat32(
60
- values: unknown[],
61
- error: () => Error,
62
- ): Float32Array {
63
- const result = new Float32Array(values.length);
64
- for (let index = 0; index < values.length; index++) {
65
- const value = values[index];
66
- if (typeof value !== "number" || !Number.isFinite(value)) throw error();
67
- result[index] = value;
68
- if (!Number.isFinite(result[index]!)) throw error();
69
- }
70
- return result;
71
- }
72
-
73
- function parseEmbeddingVectors(
74
- value: unknown,
75
- inputCount: number,
76
- dimension: number,
77
- ): Float32Array[] {
78
- const response = value as { data?: unknown };
79
- if (
80
- typeof response !== "object" ||
81
- response === null ||
82
- !Array.isArray(response.data) ||
83
- response.data.length !== inputCount
84
- ) {
85
- throw new RemoteInferenceError(
86
- "protocol",
87
- "embedding endpoint returned an incomplete data array",
88
- );
89
- }
90
-
91
- const vectors = new Array<Float32Array>(inputCount);
92
- const seen = new Set<number>();
93
- for (const value of response.data) {
94
- const entry = value as { index?: unknown; embedding?: unknown };
95
- if (
96
- typeof entry !== "object" ||
97
- entry === null ||
98
- !Number.isInteger(entry.index) ||
99
- (entry.index as number) < 0 ||
100
- (entry.index as number) >= inputCount ||
101
- seen.has(entry.index as number) ||
102
- !Array.isArray(entry.embedding) ||
103
- entry.embedding.length !== dimension
104
- ) {
105
- throw new RemoteInferenceError(
106
- "protocol",
107
- "embedding endpoint returned invalid indexed vectors",
108
- );
109
- }
110
- seen.add(entry.index as number);
111
- vectors[entry.index as number] = finiteFloat32(
112
- entry.embedding,
113
- () => new RemoteInferenceError("protocol", "embedding endpoint returned invalid vector values"),
114
- );
115
- }
116
- return vectors;
117
- }
118
-
119
- export function parseLocalEmbeddingVectors(
120
- value: unknown,
121
- inputCount: number,
122
- dimension: number,
123
- ): Float32Array[] {
124
- if (!Array.isArray(value) || value.length !== inputCount) {
125
- throw new Error("Embedding model returned an unexpected batch size.");
126
- }
127
- return value.map((row) => {
128
- if (!Array.isArray(row) || row.length !== dimension) {
129
- throw new Error("Embedding model returned unexpected vector dimensions.");
130
- }
131
- return finiteFloat32(
132
- row,
133
- () => new Error("Embedding model returned invalid vector values."),
134
- );
135
- });
136
- }
137
-
138
- function rerankerRequestBody(
139
- reranker: RemoteRerankerConfig,
140
- query: string,
141
- docs: { skill_id: string; text: string }[],
142
- ): Record<string, unknown> {
143
- if (reranker.adapter === "jina-v1") {
144
- return {
145
- model: reranker.model,
146
- query,
147
- documents: docs.map((doc) => doc.text),
148
- };
149
- }
150
- return {
151
- model: reranker.model,
152
- query,
153
- documents: docs.map((doc) => ({
154
- text: doc.text,
155
- id: doc.skill_id,
156
- meta: {},
157
- })),
158
- top_n: docs.length,
159
- return_documents: false,
160
- };
161
- }
162
-
163
- function parseRerankerScores(
164
- adapter: RemoteRerankerConfig["adapter"],
165
- value: unknown,
166
- documentCount: number,
167
- ): number[] {
168
- const response = value as { results?: unknown };
169
- if (
170
- typeof response !== "object" ||
171
- response === null ||
172
- !Array.isArray(response.results) ||
173
- response.results.length !== documentCount
174
- ) {
175
- throw new RemoteInferenceError(
176
- "protocol",
177
- `reranker adapter "${adapter}" returned an incomplete results array`,
178
- );
179
- }
180
-
181
- const scores = new Array<number>(documentCount);
182
- const seen = new Set<number>();
183
- for (const value of response.results) {
184
- const result = value as { index?: unknown; relevance_score?: unknown };
185
- if (
186
- typeof result !== "object" ||
187
- result === null ||
188
- !Number.isInteger(result.index) ||
189
- (result.index as number) < 0 ||
190
- (result.index as number) >= documentCount ||
191
- seen.has(result.index as number) ||
192
- typeof result.relevance_score !== "number" ||
193
- !Number.isFinite(result.relevance_score)
194
- ) {
195
- throw new RemoteInferenceError(
196
- "protocol",
197
- `reranker adapter "${adapter}" returned invalid indexed scores`,
198
- );
199
- }
200
- seen.add(result.index as number);
201
- scores[result.index as number] = result.relevance_score;
202
- }
203
- return scores;
204
- }
205
-
206
- async function fetchRerankerScores(
207
- reranker: RemoteRerankerConfig,
208
- timeoutMs: number,
209
- query: string,
210
- docs: { skill_id: string; text: string }[],
211
- allowedHosts: string[] | undefined,
212
- ): Promise<number[]> {
213
- if (docs.length === 0) return [];
214
-
215
- assertInferenceHostAllowed(reranker.endpoint, allowedHosts);
216
-
217
- let response: Response;
218
- try {
219
- response = await fetch(reranker.endpoint, {
220
- method: "POST",
221
- headers: {
222
- "content-type": "application/json",
223
- ...authorizationHeaders(reranker.api_key_env, "inference.reranker.api_key_env"),
224
- },
225
- body: JSON.stringify(rerankerRequestBody(reranker, query, docs)),
226
- signal: AbortSignal.timeout(timeoutMs),
227
- });
228
- } catch (error) {
229
- if (error instanceof RemoteInferenceError) throw error;
230
- const isTimeout =
231
- (error as { name?: string })?.name === "TimeoutError" ||
232
- (error as { name?: string })?.name === "AbortError" ||
233
- String(error).toLowerCase().includes("timeout") ||
234
- String(error).toLowerCase().includes("aborted");
235
- if (isTimeout) {
236
- throw new RemoteInferenceError(
237
- "availability",
238
- `reranker adapter "${reranker.adapter}" request timed out`,
239
- );
240
- }
241
- throw new RemoteInferenceError(
242
- "availability",
243
- `reranker adapter "${reranker.adapter}" request failed`,
244
- );
245
- }
246
- if (!response.ok) {
247
- throw httpFailure(`reranker adapter "${reranker.adapter}"`, response.status);
248
- }
249
-
250
- let parsed: unknown;
251
- try {
252
- parsed = await response.json();
253
- } catch {
254
- throw new RemoteInferenceError(
255
- "protocol",
256
- `reranker adapter "${reranker.adapter}" returned malformed JSON`,
257
- );
258
- }
259
- return parseRerankerScores(reranker.adapter, parsed, docs.length);
260
- }
261
-
262
- function localInference(config: Config) {
263
- if (config.inference.mode !== "local") throw new Error("Local inference is not configured.");
264
- return config.inference;
265
- }
266
-
267
- async function setupTransformers(cacheDir: string) {
268
- process.env.HF_HUB_CACHE = cacheDir;
269
- process.env.HF_HOME = cacheDir;
270
-
271
- const { env, pipeline } = await import("@huggingface/transformers");
272
- env.cacheDir = cacheDir;
273
- return pipeline;
274
- }
275
-
276
- async function getLocalEmbedder(config: Config): Promise<FeatureExtractor> {
277
- if (localEmbedder) return localEmbedder;
278
-
279
- const inference = localInference(config);
280
- const cacheDir = expandHome(inference.models_dir);
281
- const pipeline = await setupTransformers(cacheDir);
282
-
283
- localEmbedder = await pipeline("feature-extraction", inference.embedding.model, {
284
- device: inference.embedding.device || "cpu",
285
- dtype: inference.embedding.dtype || "q8",
286
- });
287
- return localEmbedder;
288
- }
289
-
290
- /**
291
- * Real HTTP clients or in-process local ONNX inference clients.
292
- * Every remote HTTP call is bounded by inference.timeout_ms; timeouts and transport
293
- * errors reject so resolveSkill can fall back to the strongest available retrieval lane.
294
- * Local ONNX calls run in-process using @huggingface/transformers.
295
- */
296
- export function createClients(config: Config): Clients {
297
- const clients: Clients = {
298
- async embed(texts: string[]): Promise<Float32Array[]> {
299
- if (config.inference.mode === "local") {
300
- const pipe = await getLocalEmbedder(config);
301
- const output = await pipe(texts, { pooling: "mean", normalize: true });
302
- const dim = output.dims[1];
303
- if (
304
- dim === undefined ||
305
- output.dims.length !== 2 ||
306
- output.dims[0] !== texts.length ||
307
- dim !== config.inference.embedding.dimension
308
- ) {
309
- throw new Error(`Embedding model returned unexpected dimensions: ${output.dims.join("x")}`);
310
- }
311
- const rows = Array.from({ length: texts.length }, (_, index) =>
312
- output.slice(index, null).tolist(),
313
- );
314
- return parseLocalEmbeddingVectors(rows, texts.length, dim);
315
- }
316
-
317
- const embedding = config.inference.embedding;
318
- assertInferenceHostAllowed(embedding.endpoint, config.egress?.allowed_hosts);
319
- let response: Response;
320
- try {
321
- response = await fetch(embedding.endpoint, {
322
- method: "POST",
323
- headers: {
324
- "content-type": "application/json",
325
- ...authorizationHeaders(embedding.api_key_env, "inference.embedding.api_key_env"),
326
- },
327
- body: JSON.stringify({ model: embedding.model, input: texts }),
328
- signal: AbortSignal.timeout(config.inference.timeout_ms),
329
- });
330
- } catch (error) {
331
- if (error instanceof RemoteInferenceError) throw error;
332
- const isTimeout =
333
- (error as { name?: string })?.name === "TimeoutError" ||
334
- (error as { name?: string })?.name === "AbortError" ||
335
- String(error).toLowerCase().includes("timeout") ||
336
- String(error).toLowerCase().includes("aborted");
337
- if (isTimeout) {
338
- throw new RemoteInferenceError("availability", "embedding endpoint request timed out");
339
- }
340
- throw new RemoteInferenceError("availability", "embedding endpoint request failed");
341
- }
342
- if (!response.ok) throw httpFailure("embedding endpoint", response.status);
343
-
344
- let parsed: unknown;
345
- try {
346
- parsed = await response.json();
347
- } catch {
348
- throw new RemoteInferenceError("protocol", "embedding endpoint returned malformed JSON");
349
- }
350
- return parseEmbeddingVectors(parsed, texts.length, embedding.dimension);
351
- },
352
- };
353
- if (config.inference.mode === "remote" && config.inference.reranker) {
354
- const inference = config.inference;
355
- clients.rerank = async (query, docs) => {
356
- const reranker = inference.reranker;
357
- if (!reranker) throw new Error("Reranker is not configured.");
358
- return fetchRerankerScores(
359
- reranker,
360
- inference.timeout_ms,
361
- query,
362
- docs,
363
- config.egress?.allowed_hosts,
364
- );
365
- };
366
- }
367
- return clients;
368
- }
@@ -1,85 +0,0 @@
1
- import { emitSuccess } from "../output";
2
- import { confirmIfNeeded } from "./shared";
3
- import type { ContextAdapter } from "../adapters";
4
- import type { ResolvedContext } from "../context";
5
- import { isGlobalFlag, isGlobalFlagWithValue } from "../global-flags";
6
-
7
- export async function runAudit(
8
- subCommand: string,
9
- args: string[],
10
- options: { isJson: boolean; dryRun: boolean; context: ResolvedContext; adapter: ContextAdapter },
11
- ): Promise<void> {
12
- if (subCommand !== "prune") {
13
- throw new Error("usage: skillmux audit prune [--older-than <window>] [--dry-run] [--yes] [--json]");
14
- }
15
-
16
- let olderThan: string | undefined;
17
- let dryRun = options.dryRun;
18
- let yes = false;
19
- for (let i = 0; i < args.length; i++) {
20
- const arg = args[i];
21
- if (arg === "--older-than") olderThan = args[++i];
22
- else if (arg === "--dry-run") dryRun = true;
23
- else if (arg === "--yes") yes = true;
24
- else if (isGlobalFlag(arg, "--json", "--allow-insecure", "--verbose")) {
25
- // handled globally by main()'s flags; recognized here so it isn't rejected
26
- } else if (isGlobalFlagWithValue(arg)) {
27
- i++; // skip flag value
28
- } else if (arg?.startsWith("--")) {
29
- throw new Error(`unknown audit prune option: ${arg}`);
30
- }
31
- }
32
-
33
- if (dryRun) {
34
- const counts = await options.adapter.auditCount(olderThan);
35
- emitSuccess(
36
- { isJson: options.isJson },
37
- counts,
38
- () =>
39
- console.log(
40
- counts.cutoff === null
41
- ? "prune: audit.retention_days is 0 (pruning disabled); nothing to do"
42
- : `prune: audit=${counts.audit_deleted} fetch=${counts.fetch_deleted} admin_audit=${counts.admin_audit_deleted} (dry-run)`,
43
- ),
44
- );
45
- return;
46
- }
47
-
48
- // Pre-fetch count / cutoff info for confirmation prompt if local or remote
49
- const countResult = await options.adapter.auditCount(olderThan);
50
- if (countResult.cutoff === null) {
51
- emitSuccess(
52
- { isJson: options.isJson },
53
- { audit_deleted: 0, fetch_deleted: 0, admin_audit_deleted: 0, dry_run: false, cutoff: null },
54
- () => console.log("prune: audit.retention_days is 0 (pruning disabled); nothing to do"),
55
- );
56
- return;
57
- }
58
-
59
- if (
60
- !(await confirmIfNeeded({
61
- confirmed: yes,
62
- isJson: options.isJson,
63
- prompt: `prune audit rows older than ${countResult.cutoff}?`,
64
- nonInteractiveError: "skillmux audit prune requires --yes when run non-interactively",
65
- }))
66
- )
67
- return;
68
-
69
- // Reuse the exact cutoff already shown in the confirmation prompt — recomputing
70
- // it here (if olderThan was left unset) could drift from what was confirmed,
71
- // e.g. if audit.retention_days changed between the two calls on a remote target.
72
- const counts = await options.adapter.auditPrune({
73
- older_than: countResult.cutoff,
74
- dry_run: false,
75
- confirm: true,
76
- });
77
- emitSuccess(
78
- { isJson: options.isJson },
79
- counts,
80
- () =>
81
- console.log(
82
- `prune: audit=${counts.audit_deleted} fetch=${counts.fetch_deleted} admin_audit=${counts.admin_audit_deleted}`,
83
- ),
84
- );
85
- }
@@ -1,224 +0,0 @@
1
- import { expandHome, migrateLegacyPaths, resolveConfigPath } from "../config";
2
- import { type ContextAdapter } from "../adapters";
3
- import { type ResolvedContext } from "../context";
4
- import { applyConfigInit, planConfigInit, type ConfigInitPlan } from "../setup";
5
- import { emitSuccess, isInteractive, renderContextBanner, unknownSubcommandError } from "../output";
6
- import { confirmAction } from "./shared";
7
- import { isGlobalFlag } from "../global-flags";
8
- function emitConfigInitOutcome(
9
- ctx: { isJson: boolean },
10
- opts: {
11
- phase: "plan" | "result";
12
- dryRun: boolean;
13
- applied: boolean;
14
- plan: ConfigInitPlan;
15
- action: "create" | "preserve";
16
- text: string;
17
- },
18
- ): void {
19
- if (ctx.isJson) {
20
- console.log(
21
- JSON.stringify({
22
- schema_version: 1,
23
- ok: true,
24
- command: "config init",
25
- phase: opts.phase,
26
- dry_run: opts.dryRun,
27
- applied: opts.applied,
28
- plan: {
29
- config_path: opts.plan.configPath,
30
- vault_path: opts.plan.vaultPath,
31
- action: opts.action,
32
- },
33
- }),
34
- );
35
- return;
36
- }
37
- console.log(opts.text);
38
- }
39
-
40
- export async function handleConfigCommand(
41
- adapter: ContextAdapter,
42
- sub: string,
43
- args: string[],
44
- ctx: { context: ResolvedContext; isJson: boolean; dryRun: boolean },
45
- ) {
46
- if (sub === "init") {
47
- let vaultPath: string | undefined;
48
- let yes = false;
49
- for (let i = 0; i < args.length; i++) {
50
- const option = args[i];
51
- if (option === "--vault") {
52
- vaultPath = args[++i];
53
- if (!vaultPath)
54
- throw new Error("usage: skillmux config init --vault <path> --yes");
55
- } else if (option === "--yes") {
56
- yes = true;
57
- } else if (isGlobalFlag(option, "--dry-run", "--json")) {
58
- continue;
59
- } else {
60
- throw new Error(`unknown config init option: ${option}`);
61
- }
62
- }
63
- if (!vaultPath) {
64
- if (isInteractive() && !ctx.isJson) {
65
- vaultPath = "~/skills";
66
- } else {
67
- throw new Error("usage: skillmux config init --vault <path> --yes");
68
- }
69
- }
70
-
71
- migrateLegacyPaths();
72
- const plan = planConfigInit(resolveConfigPath(), expandHome(vaultPath));
73
- if (plan.action === "preserve") {
74
- emitConfigInitOutcome(ctx, {
75
- phase: "result",
76
- dryRun: ctx.dryRun,
77
- applied: false,
78
- plan,
79
- action: "preserve",
80
- text: `preserved existing config: ${plan.configPath}`,
81
- });
82
- return;
83
- }
84
- if (ctx.dryRun) {
85
- emitConfigInitOutcome(ctx, {
86
- phase: "plan",
87
- dryRun: true,
88
- applied: false,
89
- plan,
90
- action: "create",
91
- text: `config create: ${plan.configPath} (dry-run)`,
92
- });
93
- return;
94
- }
95
- if (!yes) {
96
- if (!ctx.isJson && isInteractive()) {
97
- if (
98
- !(await confirmAction(
99
- `create ${plan.configPath} with vault_path ${plan.vaultPath}?`,
100
- ))
101
- ) {
102
- console.log("config init cancelled; nothing written");
103
- return;
104
- }
105
- } else {
106
- throw new Error(
107
- "config initialization requires --yes in noninteractive mode",
108
- );
109
- }
110
- }
111
-
112
- const result = applyConfigInit(plan);
113
- emitConfigInitOutcome(ctx, {
114
- phase: "result",
115
- dryRun: false,
116
- applied: result === "created",
117
- plan,
118
- action: plan.action,
119
- text:
120
- result === "created"
121
- ? `created ${plan.configPath}`
122
- : `preserved existing config: ${plan.configPath}`,
123
- });
124
- return;
125
- }
126
-
127
- if (sub === "show") {
128
- const withSources = args.includes("--sources");
129
- const data = await adapter.getConfigShow();
130
- emitSuccess({ isJson: ctx.isJson, context: ctx.context }, data, () => {
131
- renderContextBanner(ctx.context);
132
- if (withSources) {
133
- const policy =
134
- data.effective.config?.environment_overrides === false
135
- ? "strict (TOML authoritative)"
136
- : "permissive (environment overrides enabled)";
137
- console.log(`Policy: ${policy}`);
138
- console.log("\nSources:");
139
- for (const [k, src] of Object.entries(data.sources)) {
140
- console.log(` ${k}: ${src}`);
141
- }
142
- console.log("\nEffective Configuration:");
143
- }
144
- console.log(JSON.stringify(data.effective, null, 2));
145
- });
146
- return;
147
- }
148
-
149
- if (sub === "get") {
150
- const key = args[0];
151
- if (!key) throw new Error("usage: skillmux config get <key>");
152
- const val = await adapter.getConfigGet(key);
153
- emitSuccess(
154
- { isJson: ctx.isJson, context: ctx.context },
155
- { key, value: val },
156
- () => {
157
- console.log(
158
- typeof val === "object" ? JSON.stringify(val) : String(val),
159
- );
160
- },
161
- );
162
- return;
163
- }
164
-
165
- if (sub === "validate") {
166
- const res = await adapter.configValidate();
167
- emitSuccess({ isJson: ctx.isJson, context: ctx.context }, res, () => {
168
- console.log(res.valid ? "configuration is valid" : "configuration is invalid");
169
- });
170
- return;
171
- }
172
-
173
- if (sub === "diff") {
174
- const res = await adapter.configDiff();
175
- emitSuccess({ isJson: ctx.isJson, context: ctx.context }, res, () => {
176
- renderContextBanner(ctx.context);
177
- console.log(JSON.stringify(res.diff, null, 2));
178
- });
179
- return;
180
- }
181
-
182
- if (sub === "set") {
183
- const key = args[0];
184
- const value = args[1];
185
- if (!key || value === undefined) {
186
- throw new Error("usage: skillmux config set <key> <value> [--dry-run]");
187
- }
188
- const res = await adapter.configSet(key, value, { dryRun: ctx.dryRun });
189
- emitSuccess({ isJson: ctx.isJson, context: ctx.context }, res, () => {
190
- renderContextBanner(ctx.context);
191
- const prefix = ctx.dryRun ? "[dry-run] " : "";
192
- console.log(
193
- `${prefix}${key}: ${JSON.stringify(res.prior_val)} -> ${JSON.stringify(res.resulting_val)}`,
194
- );
195
- console.log(
196
- `Persistence: ${res.persistence}, Application: ${res.application}`,
197
- );
198
- });
199
- return;
200
- }
201
-
202
- if (sub === "status") {
203
- const res = await adapter.configStatus();
204
- emitSuccess({ isJson: ctx.isJson, context: ctx.context }, res, () => {
205
- renderContextBanner(ctx.context);
206
- console.log(`runtime: ${res.runtime}`);
207
- console.log(`deployment runtime: ${res.deployment_runtime}`);
208
- console.log(`image variant: ${res.image_variant ?? "none"}`);
209
- console.log(`active revision: ${res.active_revision}`);
210
- console.log(`readiness: ${res.readiness.status}`);
211
- });
212
- return;
213
- }
214
-
215
- throw unknownSubcommandError("config", sub, [
216
- "init",
217
- "show",
218
- "get",
219
- "set",
220
- "validate",
221
- "diff",
222
- "status",
223
- ]);
224
- }