@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
@@ -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
- }