@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.
- package/CHANGELOG.md +15 -0
- package/README.md +20 -10
- package/bin/skillmux.js +89 -0
- package/docs/README.md +4 -4
- package/docs/cli.md +15 -5
- package/docs/concepts.md +4 -3
- package/docs/configuration.md +22 -5
- package/docs/deployment.md +3 -3
- package/docs/getting-started.md +32 -7
- package/docs/releasing.md +17 -2
- package/docs/sdd/cli-remote-target-parity/schema-bucket-b.yaml +297 -0
- package/docs/sdd/cli-remote-target-parity/spec-bucket-b.md +95 -0
- package/docs/sdd/cli-remote-target-parity/spec.md +111 -0
- package/docs/sdd/cli-remote-target-parity/think.md +183 -0
- package/docs/sdd/cli-surface-consistency/think.md +149 -0
- package/docs/sdd/config-authority-rerank-resilience/plan.md +338 -0
- package/docs/sdd/config-authority-rerank-resilience/review.md +124 -0
- package/docs/sdd/config-authority-rerank-resilience/spec.md +78 -0
- package/docs/sdd/feat-packaging-deployment/review.md +95 -0
- package/docs/sdd/fetch-outcome-flywheel/review.md +103 -0
- package/docs/sdd/fetch-outcome-flywheel/schema.json +347 -0
- package/docs/sdd/fetch-outcome-flywheel/spec.md +185 -0
- package/docs/sdd/npm-native-binaries/review.md +103 -0
- package/docs/sdd/npm-native-binaries/spec.md +148 -0
- package/docs/sdd/ranked-shortlist-2/pr3-calibration-removal.md +56 -0
- package/docs/sdd/refactor-builtin-target-paths/spec.md +41 -0
- package/docs/sdd/runtime-resource-hardening/review.md +86 -0
- package/docs/sdd/runtime-resource-hardening/spec.md +43 -0
- package/docs/sdd/security-hardening-phase/think.md +129 -0
- package/docs/sdd/security-observability/review.md +96 -0
- package/docs/sdd/security-observability/spec.md +55 -0
- package/docs/sdd/skill-provenance-update/review.md +152 -0
- package/docs/sdd/skill-provenance-update/schema.json +193 -0
- package/docs/sdd/skill-provenance-update/spec.md +134 -0
- package/docs/sdd/supply-chain-hardening/review.md +50 -0
- package/docs/sdd/supply-chain-hardening/spec.md +42 -0
- package/docs/sdd/target-marker-rehome/spec.md +33 -0
- package/docs/skill-management.md +1 -1
- package/docs/troubleshooting.md +18 -2
- package/package.json +15 -8
- package/src/adapters.ts +0 -438
- package/src/audit.ts +0 -21
- package/src/cli.ts +0 -928
- package/src/clients.ts +0 -368
- package/src/commands/audit.ts +0 -85
- package/src/commands/config.ts +0 -224
- package/src/commands/context.ts +0 -104
- package/src/commands/core.ts +0 -56
- package/src/commands/doctor.ts +0 -97
- package/src/commands/eval.ts +0 -88
- package/src/commands/init.ts +0 -672
- package/src/commands/install.ts +0 -155
- package/src/commands/local-vault.ts +0 -60
- package/src/commands/models.ts +0 -10
- package/src/commands/outdated.ts +0 -115
- package/src/commands/project.ts +0 -580
- package/src/commands/report.ts +0 -66
- package/src/commands/scan.ts +0 -71
- package/src/commands/shared.ts +0 -38
- package/src/commands/skill.ts +0 -33
- package/src/commands/sync.ts +0 -233
- package/src/commands/target.ts +0 -225
- package/src/commands/update.ts +0 -277
- package/src/completions.ts +0 -196
- package/src/concurrency-limiter.ts +0 -61
- package/src/config-service.ts +0 -398
- package/src/config-watcher.ts +0 -253
- package/src/config.ts +0 -583
- package/src/context.ts +0 -188
- package/src/db-audit.ts +0 -286
- package/src/db-index.ts +0 -238
- package/src/db.ts +0 -3
- package/src/deployment.ts +0 -39
- package/src/doctor.ts +0 -205
- package/src/eval.ts +0 -302
- package/src/global-flags.ts +0 -46
- package/src/init-agents.ts +0 -329
- package/src/init-instructions.ts +0 -192
- package/src/init.ts +0 -389
- package/src/install.ts +0 -232
- package/src/lifecycle.ts +0 -51
- package/src/logger.ts +0 -26
- package/src/manifest.ts +0 -352
- package/src/mcp-registration.ts +0 -89
- package/src/metrics.ts +0 -121
- package/src/models.ts +0 -20
- package/src/output.ts +0 -214
- package/src/project-setup.ts +0 -36
- package/src/prompts.ts +0 -124
- package/src/provenance.ts +0 -99
- package/src/rate-limiter.ts +0 -137
- package/src/readiness.ts +0 -30
- package/src/redact.ts +0 -52
- package/src/router-core.ts +0 -666
- package/src/rrf.ts +0 -31
- package/src/scan.ts +0 -300
- package/src/server.ts +0 -930
- package/src/setup.ts +0 -145
- package/src/snapshot.ts +0 -135
- package/src/stats.ts +0 -340
- package/src/sync.ts +0 -482
- package/src/toml-writer.ts +0 -51
- package/src/types.ts +0 -212
- package/src/vault.ts +0 -207
package/src/mcp-registration.ts
DELETED
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
getAgentDefinition,
|
|
3
|
-
SUPPORTED_AGENT_IDS,
|
|
4
|
-
type AgentId,
|
|
5
|
-
type McpRegistrationScope,
|
|
6
|
-
} from "./init-agents";
|
|
7
|
-
|
|
8
|
-
export type { McpRegistrationScope };
|
|
9
|
-
|
|
10
|
-
export const MCP_REGISTRABLE_AGENTS = SUPPORTED_AGENT_IDS.filter(
|
|
11
|
-
(agent) => getAgentDefinition(agent).mcpRegistration !== undefined,
|
|
12
|
-
);
|
|
13
|
-
|
|
14
|
-
export const MCP_PROJECT_REGISTRABLE_AGENTS = MCP_REGISTRABLE_AGENTS.filter(
|
|
15
|
-
(agent) => getAgentDefinition(agent).mcpRegistration!.buildArgs("project") !== undefined,
|
|
16
|
-
);
|
|
17
|
-
|
|
18
|
-
export function isMcpRegistrable(agent: AgentId): boolean {
|
|
19
|
-
return getAgentDefinition(agent).mcpRegistration !== undefined;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface McpRegistrationResult {
|
|
23
|
-
agent: AgentId;
|
|
24
|
-
ok: boolean;
|
|
25
|
-
error?: string;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
type SpawnFn = (
|
|
29
|
-
cmd: string[],
|
|
30
|
-
opts?: { cwd?: string },
|
|
31
|
-
) => {
|
|
32
|
-
exited: Promise<number>;
|
|
33
|
-
stderr: ReadableStream<Uint8Array> | number;
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Runs the agent's own CLI to register skillmux as an MCP server. Never
|
|
38
|
-
* throws — registration failure (tool not installed, command rejected,
|
|
39
|
-
* etc.) is reported in the result, not fatal to the caller's larger flow.
|
|
40
|
-
*/
|
|
41
|
-
export async function registerMcpServer(
|
|
42
|
-
agent: AgentId,
|
|
43
|
-
options: {
|
|
44
|
-
spawn?: SpawnFn;
|
|
45
|
-
scope?: McpRegistrationScope;
|
|
46
|
-
cwd?: string;
|
|
47
|
-
} = {},
|
|
48
|
-
): Promise<McpRegistrationResult> {
|
|
49
|
-
const entry = getAgentDefinition(agent).mcpRegistration;
|
|
50
|
-
const scope = options.scope ?? "user";
|
|
51
|
-
const args = entry?.buildArgs(scope);
|
|
52
|
-
if (!entry || !args) {
|
|
53
|
-
return {
|
|
54
|
-
agent,
|
|
55
|
-
ok: false,
|
|
56
|
-
error: `no ${scope === "project" ? "project-scoped " : ""}MCP registration command known for agent "${agent}"`,
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
const spawn: SpawnFn =
|
|
60
|
-
options.spawn ??
|
|
61
|
-
((cmd, opts) => Bun.spawn(cmd, { stdout: "pipe", stderr: "pipe", cwd: opts?.cwd }));
|
|
62
|
-
try {
|
|
63
|
-
const proc = spawn([entry.command, ...args], { cwd: options.cwd });
|
|
64
|
-
const stderrText =
|
|
65
|
-
typeof proc.stderr === "number"
|
|
66
|
-
? ""
|
|
67
|
-
: await new Response(proc.stderr).text();
|
|
68
|
-
const exitCode = await proc.exited;
|
|
69
|
-
if (exitCode !== 0) {
|
|
70
|
-
return {
|
|
71
|
-
agent,
|
|
72
|
-
ok: false,
|
|
73
|
-
error:
|
|
74
|
-
stderrText.trim() || `${entry.command} exited with code ${exitCode}`,
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
return { agent, ok: true };
|
|
78
|
-
} catch (error) {
|
|
79
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
80
|
-
const notFound = /ENOENT|not found|no such file/i.test(message);
|
|
81
|
-
return {
|
|
82
|
-
agent,
|
|
83
|
-
ok: false,
|
|
84
|
-
error: notFound
|
|
85
|
-
? `"${entry.command}" is not installed or not on PATH`
|
|
86
|
-
: message,
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
}
|
package/src/metrics.ts
DELETED
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
import type { ReadinessSnapshot } from "./readiness";
|
|
2
|
-
import type { DeploymentIdentity } from "./deployment";
|
|
3
|
-
|
|
4
|
-
type MetricsDeploymentIdentity = Pick<
|
|
5
|
-
DeploymentIdentity,
|
|
6
|
-
"version" | "runtime" | "image_variant"
|
|
7
|
-
>;
|
|
8
|
-
|
|
9
|
-
export class MetricsRegistry {
|
|
10
|
-
private requests = new Map<string, number>();
|
|
11
|
-
|
|
12
|
-
private buckets = [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0];
|
|
13
|
-
private latencyBuckets = new Array(this.buckets.length).fill(0);
|
|
14
|
-
private latencyInf = 0;
|
|
15
|
-
private latencySum = 0;
|
|
16
|
-
private latencyCount = 0;
|
|
17
|
-
|
|
18
|
-
private errors = 0;
|
|
19
|
-
private rateLimitsExceeded = 0;
|
|
20
|
-
private degradations = new Map<string, number>();
|
|
21
|
-
private readiness: ReadinessSnapshot | null = null;
|
|
22
|
-
private deployment: MetricsDeploymentIdentity | null = null;
|
|
23
|
-
|
|
24
|
-
setReadiness(readiness: ReadinessSnapshot) {
|
|
25
|
-
this.readiness = readiness;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
setDeployment(deployment: MetricsDeploymentIdentity) {
|
|
29
|
-
this.deployment = deployment;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
recordRequest(method: string) {
|
|
33
|
-
this.requests.set(method, (this.requests.get(method) || 0) + 1);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
recordDegradation(stage: "embedding" | "reranker", reason: string) {
|
|
37
|
-
const key = `${stage}:${reason}`;
|
|
38
|
-
this.degradations.set(key, (this.degradations.get(key) || 0) + 1);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
recordResolveLatencySeconds(seconds: number) {
|
|
42
|
-
this.latencySum += seconds;
|
|
43
|
-
this.latencyCount++;
|
|
44
|
-
for (let i = 0; i < this.buckets.length; i++) {
|
|
45
|
-
if (seconds <= this.buckets[i]!) {
|
|
46
|
-
this.latencyBuckets[i]++;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
this.latencyInf++;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
recordError() {
|
|
53
|
-
this.errors++;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
recordRateLimitExceeded() {
|
|
57
|
-
this.rateLimitsExceeded++;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
render(): string {
|
|
61
|
-
const lines: string[] = [];
|
|
62
|
-
|
|
63
|
-
// Requests total
|
|
64
|
-
lines.push("# HELP skill_router_requests_total Total count of incoming MCP requests.");
|
|
65
|
-
lines.push("# TYPE skill_router_requests_total counter");
|
|
66
|
-
for (const [method, count] of this.requests) {
|
|
67
|
-
lines.push(`skill_router_requests_total{method="${method}"} ${count}`);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Latency histogram
|
|
71
|
-
lines.push("# HELP skill_router_resolve_latency_seconds Latency histogram of resolve_skill executions.");
|
|
72
|
-
lines.push("# TYPE skill_router_resolve_latency_seconds histogram");
|
|
73
|
-
for (let i = 0; i < this.buckets.length; i++) {
|
|
74
|
-
let le = this.buckets[i]!.toString();
|
|
75
|
-
// Format to remove trailing .0 if integer to match test
|
|
76
|
-
if (Number.isInteger(this.buckets[i]!)) {
|
|
77
|
-
le = this.buckets[i]!.toFixed(0);
|
|
78
|
-
}
|
|
79
|
-
lines.push(`skill_router_resolve_latency_seconds_bucket{le="${le}"} ${this.latencyBuckets[i]}`);
|
|
80
|
-
}
|
|
81
|
-
lines.push(`skill_router_resolve_latency_seconds_bucket{le="+Inf"} ${this.latencyInf}`);
|
|
82
|
-
lines.push(`skill_router_resolve_latency_seconds_sum ${this.latencySum}`);
|
|
83
|
-
lines.push(`skill_router_resolve_latency_seconds_count ${this.latencyCount}`);
|
|
84
|
-
|
|
85
|
-
// Errors total
|
|
86
|
-
lines.push("# HELP skill_router_errors_total Total count of server and query routing errors.");
|
|
87
|
-
lines.push("# TYPE skill_router_errors_total counter");
|
|
88
|
-
lines.push(`skill_router_errors_total ${this.errors}`);
|
|
89
|
-
|
|
90
|
-
// Rate limits exceeded
|
|
91
|
-
lines.push("# HELP skill_router_rate_limits_exceeded_total Total count of HTTP requests rejected by rate limiting.");
|
|
92
|
-
lines.push("# TYPE skill_router_rate_limits_exceeded_total counter");
|
|
93
|
-
lines.push(`skill_router_rate_limits_exceeded_total ${this.rateLimitsExceeded}`);
|
|
94
|
-
|
|
95
|
-
// Degraded retrieval total
|
|
96
|
-
lines.push("# HELP skill_router_degraded_retrieval_total Total count of degraded retrieval fallbacks labelled by stage and reason.");
|
|
97
|
-
lines.push("# TYPE skill_router_degraded_retrieval_total counter");
|
|
98
|
-
for (const [key, count] of this.degradations) {
|
|
99
|
-
const [stage, reason] = key.split(":");
|
|
100
|
-
lines.push(`skill_router_degraded_retrieval_total{stage="${stage}",reason="${reason}"} ${count}`);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
lines.push("# HELP skill_router_ready Whether the service is ready to route requests.");
|
|
104
|
-
lines.push("# TYPE skill_router_ready gauge");
|
|
105
|
-
lines.push(`skill_router_ready ${this.readiness?.status === "ready" ? 1 : 0}`);
|
|
106
|
-
lines.push("# HELP skill_router_retrieval_capability Active retrieval capability.");
|
|
107
|
-
lines.push("# TYPE skill_router_retrieval_capability gauge");
|
|
108
|
-
for (const capability of ["exact", "reranked", "hybrid", "lexical"]) {
|
|
109
|
-
lines.push(`skill_router_retrieval_capability{capability="${capability}"} ${this.readiness?.retrieval === capability ? 1 : 0}`);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
if (this.deployment) {
|
|
113
|
-
const imageVariant = this.deployment.image_variant ?? "none";
|
|
114
|
-
lines.push("# HELP skill_router_deployment_info Immutable deployment identity for operator comparison.");
|
|
115
|
-
lines.push("# TYPE skill_router_deployment_info gauge");
|
|
116
|
-
lines.push(`skill_router_deployment_info{version="${this.deployment.version}",runtime="${this.deployment.runtime}",image_variant="${imageVariant}"} 1`);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
return lines.join("\n") + "\n";
|
|
120
|
-
}
|
|
121
|
-
}
|
package/src/models.ts
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { expandHome } from "./config";
|
|
2
|
-
import type { Config } from "./types";
|
|
3
|
-
|
|
4
|
-
export async function downloadLocalModels(config: Config): Promise<string> {
|
|
5
|
-
if (config.inference.mode !== "local") {
|
|
6
|
-
throw new Error("models download is only available for inference.mode = \"local\".");
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
const cacheDir = expandHome(config.inference.models_dir);
|
|
10
|
-
process.env.HF_HUB_CACHE = cacheDir;
|
|
11
|
-
process.env.HF_HOME = cacheDir;
|
|
12
|
-
const { env, pipeline } = await import("@huggingface/transformers");
|
|
13
|
-
env.cacheDir = cacheDir;
|
|
14
|
-
|
|
15
|
-
await pipeline("feature-extraction", config.inference.embedding.model, {
|
|
16
|
-
device: config.inference.embedding.device ?? "cpu",
|
|
17
|
-
dtype: config.inference.embedding.dtype ?? "q8",
|
|
18
|
-
});
|
|
19
|
-
return cacheDir;
|
|
20
|
-
}
|
package/src/output.ts
DELETED
|
@@ -1,214 +0,0 @@
|
|
|
1
|
-
import type { ResolvedContext } from "./context";
|
|
2
|
-
|
|
3
|
-
export interface JsonEnvelope<T = any> {
|
|
4
|
-
schema_version: 1;
|
|
5
|
-
ok: boolean;
|
|
6
|
-
context: string | { name: string; server: string };
|
|
7
|
-
/** @deprecated Use `context` instead. Retained as a compatibility alias; no removal planned. */
|
|
8
|
-
target: string | { name: string; server: string };
|
|
9
|
-
data: T | null;
|
|
10
|
-
error: { code: string; message: string; details?: any } | null;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function formatJsonEnvelope<T>(opts: {
|
|
14
|
-
ok: boolean;
|
|
15
|
-
/** @deprecated Use `context` instead. Retained as a compatibility alias; no removal planned. */
|
|
16
|
-
target?: ResolvedContext | string | { name: string; server: string };
|
|
17
|
-
context?: ResolvedContext | string | { name: string; server: string };
|
|
18
|
-
data?: T;
|
|
19
|
-
error?: { code: string; message: string; details?: any } | null;
|
|
20
|
-
}): JsonEnvelope<T> {
|
|
21
|
-
const input: ResolvedContext | string | { name: string; server: string } =
|
|
22
|
-
opts.context ?? opts.target ?? "local";
|
|
23
|
-
let contextVal: string | { name: string; server: string };
|
|
24
|
-
if (typeof input === "string") {
|
|
25
|
-
contextVal = input;
|
|
26
|
-
} else if (typeof input === "object" && input !== null) {
|
|
27
|
-
if ("type" in input && (input as any).type === "local") {
|
|
28
|
-
contextVal = "local";
|
|
29
|
-
} else if ("name" in input && "server" in input) {
|
|
30
|
-
contextVal = { name: input.name, server: input.server };
|
|
31
|
-
} else {
|
|
32
|
-
contextVal = "local";
|
|
33
|
-
}
|
|
34
|
-
} else {
|
|
35
|
-
contextVal = "local";
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
return {
|
|
39
|
-
schema_version: 1,
|
|
40
|
-
ok: opts.ok,
|
|
41
|
-
context: contextVal,
|
|
42
|
-
target: contextVal,
|
|
43
|
-
data: opts.data ?? null,
|
|
44
|
-
error: opts.error ?? null,
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export class CliError extends Error {
|
|
49
|
-
exitCode: number;
|
|
50
|
-
code: string;
|
|
51
|
-
details?: unknown;
|
|
52
|
-
|
|
53
|
-
constructor(message: string, exitCode: number, code = `EXIT_${exitCode}`, details?: unknown) {
|
|
54
|
-
super(message);
|
|
55
|
-
this.name = "CliError";
|
|
56
|
-
this.exitCode = exitCode;
|
|
57
|
-
this.code = code;
|
|
58
|
-
this.details = details;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export function emitSuccess<T>(
|
|
63
|
-
ctx: {
|
|
64
|
-
isJson: boolean;
|
|
65
|
-
/** @deprecated Use `context` instead. Retained as a compatibility alias; no removal planned. */
|
|
66
|
-
target?: ResolvedContext | string | { name: string; server: string };
|
|
67
|
-
context?: ResolvedContext | string | { name: string; server: string };
|
|
68
|
-
},
|
|
69
|
-
data: T,
|
|
70
|
-
renderText: () => void,
|
|
71
|
-
): void {
|
|
72
|
-
if (ctx.isJson) {
|
|
73
|
-
const contextVal = ctx.context ?? ctx.target ?? "local";
|
|
74
|
-
console.log(JSON.stringify(formatJsonEnvelope({ ok: true, context: contextVal, target: contextVal, data })));
|
|
75
|
-
} else {
|
|
76
|
-
renderText();
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export function mapExitCode(err: unknown): number {
|
|
81
|
-
if (!err) return 0;
|
|
82
|
-
if (err instanceof CliError) return err.exitCode;
|
|
83
|
-
return 2;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
export function levenshteinDistance(a: string, b: string): number {
|
|
87
|
-
const m = a.length;
|
|
88
|
-
const n = b.length;
|
|
89
|
-
const dp: number[][] = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
|
|
90
|
-
|
|
91
|
-
for (let i = 0; i <= m; i++) {
|
|
92
|
-
if (!dp[i]) dp[i] = [];
|
|
93
|
-
dp[i]![0] = i;
|
|
94
|
-
}
|
|
95
|
-
for (let j = 0; j <= n; j++) {
|
|
96
|
-
if (!dp[0]) dp[0] = [];
|
|
97
|
-
dp[0]![j] = j;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
for (let i = 1; i <= m; i++) {
|
|
101
|
-
for (let j = 1; j <= n; j++) {
|
|
102
|
-
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
103
|
-
const prevRow = dp[i - 1]!;
|
|
104
|
-
const curRow = dp[i]!;
|
|
105
|
-
curRow[j] = Math.min(
|
|
106
|
-
prevRow[j]! + 1,
|
|
107
|
-
curRow[j - 1]! + 1,
|
|
108
|
-
prevRow[j - 1]! + cost
|
|
109
|
-
);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
return dp[m]![n]!;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
export function suggestCorrection(input: string, candidates: string[]): string | null {
|
|
116
|
-
let minDistance = Infinity;
|
|
117
|
-
let bestMatch: string | null = null;
|
|
118
|
-
|
|
119
|
-
for (const candidate of candidates) {
|
|
120
|
-
const dist = levenshteinDistance(input, candidate);
|
|
121
|
-
if (dist < minDistance && dist <= 2) {
|
|
122
|
-
minDistance = dist;
|
|
123
|
-
bestMatch = candidate;
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
return bestMatch;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* Builds the error for an unrecognized subcommand: "did you mean X" when
|
|
132
|
-
* close to a valid one, otherwise the full <a|b|c> usage list — never a
|
|
133
|
-
* fixed, possibly-unrelated usage string for just one of several valid
|
|
134
|
-
* subcommands (that's what `config`'s fallback used to do before this
|
|
135
|
-
* existed: any invalid subcommand got told "usage: skillmux config show",
|
|
136
|
-
* silently omitting get/set/validate/diff/status/init).
|
|
137
|
-
*/
|
|
138
|
-
export function unknownSubcommandError(
|
|
139
|
-
command: string,
|
|
140
|
-
subCommand: string,
|
|
141
|
-
validSubcommands: string[],
|
|
142
|
-
): Error {
|
|
143
|
-
const suggestion = subCommand ? suggestCorrection(subCommand, validSubcommands) : null;
|
|
144
|
-
if (suggestion) {
|
|
145
|
-
return new Error(
|
|
146
|
-
`Unknown "${command} ${subCommand}" subcommand. Did you mean "${command} ${suggestion}"?`,
|
|
147
|
-
);
|
|
148
|
-
}
|
|
149
|
-
return new Error(`usage: skillmux ${command} <${validSubcommands.join("|")}>`);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
export function isInteractive(
|
|
153
|
-
env: NodeJS.ProcessEnv = process.env,
|
|
154
|
-
stdoutIsTTY = process.stdout.isTTY,
|
|
155
|
-
): boolean {
|
|
156
|
-
return stdoutIsTTY === true && env.TERM !== "dumb";
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/** Color is opt-out only: https://no-color.org, plus the same TTY check as isInteractive(). */
|
|
160
|
-
export function isColorEnabled(
|
|
161
|
-
env: NodeJS.ProcessEnv = process.env,
|
|
162
|
-
stdoutIsTTY = process.stdout.isTTY,
|
|
163
|
-
): boolean {
|
|
164
|
-
if (env.NO_COLOR !== undefined) return false;
|
|
165
|
-
return isInteractive(env, stdoutIsTTY);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
const ANSI = { reset: "\x1b[0m", bold: "\x1b[1m", red: "\x1b[31m", yellow: "\x1b[33m", green: "\x1b[32m" } as const;
|
|
169
|
-
|
|
170
|
-
function paint(code: string, text: string): string {
|
|
171
|
-
return isColorEnabled() ? `${code}${text}${ANSI.reset}` : text;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
export const red = (text: string): string => paint(ANSI.red, text);
|
|
175
|
-
export const yellow = (text: string): string => paint(ANSI.yellow, text);
|
|
176
|
-
export const green = (text: string): string => paint(ANSI.green, text);
|
|
177
|
-
export const bold = (text: string): string => paint(ANSI.bold, text);
|
|
178
|
-
|
|
179
|
-
/** Prints a "warning: <line>" message to stderr, colored yellow when color is enabled. */
|
|
180
|
-
export function warn(line: string): void {
|
|
181
|
-
console.error(yellow(`warning: ${line}`));
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
export function renderContextBanner(context: ResolvedContext): void {
|
|
185
|
-
if (!isInteractive()) return;
|
|
186
|
-
if (context.type === "local") {
|
|
187
|
-
console.log(`Context: local`);
|
|
188
|
-
} else {
|
|
189
|
-
console.log(`Context: remote (${context.name} -> ${context.server})`);
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
export function renderTable(columns: { key: string; header: string }[], rows: Record<string, any>[]): void {
|
|
194
|
-
if (rows.length === 0) {
|
|
195
|
-
console.log("(no entries)");
|
|
196
|
-
return;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
const widths = new Map<string, number>();
|
|
200
|
-
for (const col of columns) {
|
|
201
|
-
const maxLen = Math.max(col.header.length, ...rows.map((r) => String(r[col.key] ?? "").length));
|
|
202
|
-
widths.set(col.key, maxLen);
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
const headerLine = columns.map((col) => col.header.padEnd(widths.get(col.key) ?? 0)).join(" ");
|
|
206
|
-
const sepLine = columns.map((col) => "-".repeat(widths.get(col.key) ?? 0)).join(" ");
|
|
207
|
-
|
|
208
|
-
console.log(bold(headerLine));
|
|
209
|
-
console.log(sepLine);
|
|
210
|
-
for (const row of rows) {
|
|
211
|
-
const line = columns.map((col) => String(row[col.key] ?? "").padEnd(widths.get(col.key) ?? 0)).join(" ");
|
|
212
|
-
console.log(line);
|
|
213
|
-
}
|
|
214
|
-
}
|
package/src/project-setup.ts
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { resolve } from "node:path";
|
|
2
|
-
|
|
3
|
-
interface ResolveProjectDirectoryOptions {
|
|
4
|
-
cwd?: string;
|
|
5
|
-
findGitRoot?: (cwd: string) => string | null;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
function findGitRoot(cwd: string): string | null {
|
|
9
|
-
const result = Bun.spawnSync(["git", "rev-parse", "--show-toplevel"], {
|
|
10
|
-
cwd,
|
|
11
|
-
stdout: "pipe",
|
|
12
|
-
stderr: "ignore",
|
|
13
|
-
});
|
|
14
|
-
if (result.exitCode !== 0) return null;
|
|
15
|
-
const root = result.stdout.toString().trim();
|
|
16
|
-
return root || null;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function resolveProjectDirectory(
|
|
20
|
-
explicitPath?: string,
|
|
21
|
-
options: ResolveProjectDirectoryOptions = {},
|
|
22
|
-
): string {
|
|
23
|
-
const cwd = options.cwd ?? process.cwd();
|
|
24
|
-
if (explicitPath) return resolve(explicitPath);
|
|
25
|
-
const gitRoot = (options.findGitRoot ?? findGitRoot)(cwd);
|
|
26
|
-
return resolve(gitRoot ?? cwd);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function suggestProjectName(directoryName: string): string {
|
|
30
|
-
const slug = directoryName
|
|
31
|
-
.toLowerCase()
|
|
32
|
-
.replace(/[^a-z0-9_-]+/g, "-")
|
|
33
|
-
.replace(/^-+|-+$/g, "");
|
|
34
|
-
const prefixed = /^[a-z]/.test(slug) ? slug : `project-${slug || "workspace"}`;
|
|
35
|
-
return prefixed.slice(0, 64).replace(/-+$/g, "");
|
|
36
|
-
}
|
package/src/prompts.ts
DELETED
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
import { createInterface } from "node:readline/promises";
|
|
2
|
-
import type { Readable, Writable } from "node:stream";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Streams a prompt reads from and writes to. Injectable so prompts can be
|
|
6
|
-
* tested without a terminal; defaults to the real process streams.
|
|
7
|
-
*/
|
|
8
|
-
export interface PromptIO {
|
|
9
|
-
input?: Readable;
|
|
10
|
-
output?: Writable;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export const NO_INPUT_ERROR = "no input available on stdin; re-run with --yes";
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Asks one question and resolves with the raw answer.
|
|
17
|
-
*
|
|
18
|
-
* `isInteractive()` only inspects stdout, so a caller can legitimately reach a
|
|
19
|
-
* prompt while stdin is already at EOF (a pty'd stdout under CI, a wrapper
|
|
20
|
-
* process, `skillmux init < /dev/null`). A bare `readline.question()` never
|
|
21
|
-
* settles in that case and the CLI hangs forever, so the input stream ending
|
|
22
|
-
* before an answer arrives is turned into an actionable error instead.
|
|
23
|
-
*
|
|
24
|
-
* Piped answers (`printf 'y\n' | skillmux ...`) still work: `close` only wins
|
|
25
|
-
* the race when the stream ends with the question still outstanding.
|
|
26
|
-
*/
|
|
27
|
-
export async function askQuestion(query: string, io: PromptIO = {}): Promise<string> {
|
|
28
|
-
const readline = createInterface({
|
|
29
|
-
input: io.input ?? process.stdin,
|
|
30
|
-
output: io.output ?? process.stdout,
|
|
31
|
-
});
|
|
32
|
-
let settled = false;
|
|
33
|
-
try {
|
|
34
|
-
return await new Promise<string>((resolve, reject) => {
|
|
35
|
-
readline.question(query).then(
|
|
36
|
-
(answer) => {
|
|
37
|
-
settled = true;
|
|
38
|
-
resolve(answer);
|
|
39
|
-
},
|
|
40
|
-
(error) => {
|
|
41
|
-
settled = true;
|
|
42
|
-
reject(error);
|
|
43
|
-
},
|
|
44
|
-
);
|
|
45
|
-
// Fires on EOF, and also from the `finally` below. A piped stream ends
|
|
46
|
-
// immediately after delivering its answer, so `close` can arrive before
|
|
47
|
-
// the resolution microtask runs — deferring past pending microtasks lets
|
|
48
|
-
// a real answer win the race, leaving only a genuine EOF to reject.
|
|
49
|
-
readline.once("close", () => {
|
|
50
|
-
setImmediate(() => {
|
|
51
|
-
if (settled) return;
|
|
52
|
-
settled = true;
|
|
53
|
-
reject(new Error(NO_INPUT_ERROR));
|
|
54
|
-
});
|
|
55
|
-
});
|
|
56
|
-
});
|
|
57
|
-
} finally {
|
|
58
|
-
readline.close();
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export interface SelectOption<T extends string> {
|
|
63
|
-
value: T;
|
|
64
|
-
label: string;
|
|
65
|
-
detail?: string;
|
|
66
|
-
selected?: boolean;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export function parseNumberSelection(input: string, optionCount: number): number[] {
|
|
70
|
-
if (input.trim() === "") return [];
|
|
71
|
-
const indexes = input.split(",").map((part) => {
|
|
72
|
-
const choice = Number(part.trim());
|
|
73
|
-
if (!Number.isInteger(choice) || choice < 1 || choice > optionCount) {
|
|
74
|
-
throw new Error(`select numbers between 1 and ${optionCount}, separated by commas`);
|
|
75
|
-
}
|
|
76
|
-
return choice - 1;
|
|
77
|
-
});
|
|
78
|
-
return [...new Set(indexes)].sort((a, b) => a - b);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export function parseCommaList(input: string): string[] {
|
|
82
|
-
return [...new Set(input.split(",").map((value) => value.trim()).filter(Boolean))];
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export function shouldUseWizard(
|
|
86
|
-
args: readonly string[],
|
|
87
|
-
mode: { interactive: boolean; json: boolean; dryRun: boolean },
|
|
88
|
-
): boolean {
|
|
89
|
-
if (!mode.interactive || mode.json || mode.dryRun || args.includes("--yes")) return false;
|
|
90
|
-
if (args.includes("--interactive")) return true;
|
|
91
|
-
return args.length === 0;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
export async function promptMultiSelect<T extends string>(
|
|
95
|
-
question: string,
|
|
96
|
-
options: readonly SelectOption<T>[],
|
|
97
|
-
io: PromptIO = {},
|
|
98
|
-
): Promise<T[]> {
|
|
99
|
-
const output = io.output ?? process.stdout;
|
|
100
|
-
output.write(`\n${question}\n`);
|
|
101
|
-
options.forEach((option, index) => {
|
|
102
|
-
const checked = option.selected ? "x" : " ";
|
|
103
|
-
const detail = option.detail ? ` ${option.detail}` : "";
|
|
104
|
-
output.write(` ${index + 1}. [${checked}] ${option.label}${detail}\n`);
|
|
105
|
-
});
|
|
106
|
-
const defaults = options
|
|
107
|
-
.map((option, index) => option.selected ? String(index + 1) : "")
|
|
108
|
-
.filter(Boolean)
|
|
109
|
-
.join(",");
|
|
110
|
-
const suffix = defaults ? ` [${defaults}]` : "";
|
|
111
|
-
const answer = await askQuestion(`Select numbers, comma-separated${suffix}: `, io);
|
|
112
|
-
const selection = answer.trim() === "" && defaults ? defaults : answer;
|
|
113
|
-
return parseNumberSelection(selection, options.length).map((index) => options[index]!.value);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
export async function promptText(
|
|
117
|
-
question: string,
|
|
118
|
-
defaultValue = "",
|
|
119
|
-
io: PromptIO = {},
|
|
120
|
-
): Promise<string> {
|
|
121
|
-
const suffix = defaultValue ? ` [${defaultValue}]` : "";
|
|
122
|
-
const answer = (await askQuestion(`${question}${suffix}: `, io)).trim();
|
|
123
|
-
return answer || defaultValue;
|
|
124
|
-
}
|