@klhapp/skillmux 1.12.0 → 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 (103) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +18 -9
  3. package/bin/skillmux.js +89 -0
  4. package/docs/README.md +4 -4
  5. package/docs/cli.md +14 -4
  6. package/docs/concepts.md +2 -2
  7. package/docs/configuration.md +18 -3
  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/troubleshooting.md +18 -2
  39. package/package.json +15 -8
  40. package/src/adapters.ts +0 -438
  41. package/src/audit.ts +0 -21
  42. package/src/cli.ts +0 -928
  43. package/src/clients.ts +0 -368
  44. package/src/commands/audit.ts +0 -85
  45. package/src/commands/config.ts +0 -224
  46. package/src/commands/context.ts +0 -104
  47. package/src/commands/core.ts +0 -56
  48. package/src/commands/doctor.ts +0 -97
  49. package/src/commands/eval.ts +0 -88
  50. package/src/commands/init.ts +0 -672
  51. package/src/commands/install.ts +0 -155
  52. package/src/commands/local-vault.ts +0 -60
  53. package/src/commands/models.ts +0 -10
  54. package/src/commands/outdated.ts +0 -115
  55. package/src/commands/project.ts +0 -580
  56. package/src/commands/report.ts +0 -66
  57. package/src/commands/scan.ts +0 -71
  58. package/src/commands/shared.ts +0 -38
  59. package/src/commands/skill.ts +0 -33
  60. package/src/commands/sync.ts +0 -233
  61. package/src/commands/target.ts +0 -225
  62. package/src/commands/update.ts +0 -277
  63. package/src/completions.ts +0 -196
  64. package/src/concurrency-limiter.ts +0 -61
  65. package/src/config-service.ts +0 -398
  66. package/src/config-watcher.ts +0 -253
  67. package/src/config.ts +0 -583
  68. package/src/context.ts +0 -188
  69. package/src/db-audit.ts +0 -286
  70. package/src/db-index.ts +0 -238
  71. package/src/db.ts +0 -3
  72. package/src/deployment.ts +0 -39
  73. package/src/doctor.ts +0 -205
  74. package/src/eval.ts +0 -302
  75. package/src/global-flags.ts +0 -46
  76. package/src/init-agents.ts +0 -329
  77. package/src/init-instructions.ts +0 -192
  78. package/src/init.ts +0 -390
  79. package/src/install.ts +0 -232
  80. package/src/lifecycle.ts +0 -51
  81. package/src/logger.ts +0 -26
  82. package/src/manifest.ts +0 -359
  83. package/src/mcp-registration.ts +0 -89
  84. package/src/metrics.ts +0 -121
  85. package/src/models.ts +0 -20
  86. package/src/output.ts +0 -214
  87. package/src/project-setup.ts +0 -36
  88. package/src/prompts.ts +0 -124
  89. package/src/provenance.ts +0 -99
  90. package/src/rate-limiter.ts +0 -137
  91. package/src/readiness.ts +0 -30
  92. package/src/redact.ts +0 -52
  93. package/src/router-core.ts +0 -666
  94. package/src/rrf.ts +0 -31
  95. package/src/scan.ts +0 -300
  96. package/src/server.ts +0 -930
  97. package/src/setup.ts +0 -145
  98. package/src/snapshot.ts +0 -135
  99. package/src/stats.ts +0 -340
  100. package/src/sync.ts +0 -482
  101. package/src/toml-writer.ts +0 -51
  102. package/src/types.ts +0 -212
  103. package/src/vault.ts +0 -207
@@ -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
- }
@@ -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
- }