kly 0.1.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/LICENSE +21 -0
- package/README.md +11 -0
- package/dist/ai/context.mjs +79 -0
- package/dist/ai/context.mjs.map +1 -0
- package/dist/ai/storage.mjs +50 -0
- package/dist/ai/storage.mjs.map +1 -0
- package/dist/bin/kly.d.mts +1 -0
- package/dist/bin/kly.mjs +2888 -0
- package/dist/bin/kly.mjs.map +1 -0
- package/dist/bin/launcher-vTpgdO9n.mjs +3 -0
- package/dist/bin/permissions-2r_7ZqaH.mjs +3 -0
- package/dist/cli.mjs +229 -0
- package/dist/cli.mjs.map +1 -0
- package/dist/define-app.d.mts +33 -0
- package/dist/define-app.d.mts.map +1 -0
- package/dist/define-app.mjs +183 -0
- package/dist/define-app.mjs.map +1 -0
- package/dist/index.d.mts +16 -0
- package/dist/index.mjs +15 -0
- package/dist/mcp/index.mjs +4 -0
- package/dist/mcp/schema-converter.d.mts +13 -0
- package/dist/mcp/schema-converter.d.mts.map +1 -0
- package/dist/mcp/schema-converter.mjs +30 -0
- package/dist/mcp/schema-converter.mjs.map +1 -0
- package/dist/mcp/server.d.mts +33 -0
- package/dist/mcp/server.d.mts.map +1 -0
- package/dist/mcp/server.mjs +92 -0
- package/dist/mcp/server.mjs.map +1 -0
- package/dist/permissions/index.mjs +123 -0
- package/dist/permissions/index.mjs.map +1 -0
- package/dist/sandbox/bundled-executor.d.mts +17 -0
- package/dist/sandbox/bundled-executor.d.mts.map +1 -0
- package/dist/sandbox/bundled-executor.mjs +175 -0
- package/dist/sandbox/bundled-executor.mjs.map +1 -0
- package/dist/sandbox/ipc-client.mjs +40 -0
- package/dist/sandbox/ipc-client.mjs.map +1 -0
- package/dist/sandbox/sandboxed-context.mjs +14 -0
- package/dist/sandbox/sandboxed-context.mjs.map +1 -0
- package/dist/shared/constants.mjs +36 -0
- package/dist/shared/constants.mjs.map +1 -0
- package/dist/shared/runtime-mode.mjs +59 -0
- package/dist/shared/runtime-mode.mjs.map +1 -0
- package/dist/tool.d.mts +42 -0
- package/dist/tool.d.mts.map +1 -0
- package/dist/tool.mjs +38 -0
- package/dist/tool.mjs.map +1 -0
- package/dist/types.d.mts +282 -0
- package/dist/types.d.mts.map +1 -0
- package/dist/types.mjs +19 -0
- package/dist/types.mjs.map +1 -0
- package/dist/ui/components/confirm.d.mts +13 -0
- package/dist/ui/components/confirm.d.mts.map +1 -0
- package/dist/ui/components/confirm.mjs +37 -0
- package/dist/ui/components/confirm.mjs.map +1 -0
- package/dist/ui/components/form.d.mts +50 -0
- package/dist/ui/components/form.d.mts.map +1 -0
- package/dist/ui/components/form.mjs +92 -0
- package/dist/ui/components/form.mjs.map +1 -0
- package/dist/ui/components/input.d.mts +29 -0
- package/dist/ui/components/input.d.mts.map +1 -0
- package/dist/ui/components/input.mjs +42 -0
- package/dist/ui/components/input.mjs.map +1 -0
- package/dist/ui/components/select.d.mts +41 -0
- package/dist/ui/components/select.d.mts.map +1 -0
- package/dist/ui/components/select.mjs +50 -0
- package/dist/ui/components/select.mjs.map +1 -0
- package/dist/ui/components/spinner.d.mts +28 -0
- package/dist/ui/components/spinner.d.mts.map +1 -0
- package/dist/ui/components/spinner.mjs +35 -0
- package/dist/ui/components/spinner.mjs.map +1 -0
- package/dist/ui/components/table.d.mts +60 -0
- package/dist/ui/components/table.d.mts.map +1 -0
- package/dist/ui/components/table.mjs +143 -0
- package/dist/ui/components/table.mjs.map +1 -0
- package/dist/ui/index.d.mts +9 -0
- package/dist/ui/utils/colors.d.mts +38 -0
- package/dist/ui/utils/colors.d.mts.map +1 -0
- package/dist/ui/utils/colors.mjs +64 -0
- package/dist/ui/utils/colors.mjs.map +1 -0
- package/dist/ui/utils/output.d.mts +23 -0
- package/dist/ui/utils/output.d.mts.map +1 -0
- package/dist/ui/utils/output.mjs +42 -0
- package/dist/ui/utils/output.mjs.map +1 -0
- package/dist/ui/utils/tty.d.mts +9 -0
- package/dist/ui/utils/tty.d.mts.map +1 -0
- package/dist/ui/utils/tty.mjs +12 -0
- package/dist/ui/utils/tty.mjs.map +1 -0
- package/package.json +81 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { isMCP, isSandbox } from "../../shared/runtime-mode.mjs";
|
|
2
|
+
import { isTTY } from "../utils/tty.mjs";
|
|
3
|
+
import { sendIPCRequest } from "../../sandbox/ipc-client.mjs";
|
|
4
|
+
import * as p from "@clack/prompts";
|
|
5
|
+
import pc from "picocolors";
|
|
6
|
+
|
|
7
|
+
//#region src/ui/components/form.ts
|
|
8
|
+
/**
|
|
9
|
+
* Prompt for multiple fields sequentially
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```typescript
|
|
13
|
+
* const values = await form({
|
|
14
|
+
* title: "User Registration",
|
|
15
|
+
* fields: [
|
|
16
|
+
* { name: "name", label: "Your name", type: "string", required: true },
|
|
17
|
+
* { name: "age", label: "Your age", type: "number", defaultValue: 18 },
|
|
18
|
+
* { name: "role", label: "Role", type: "enum", enumValues: ["admin", "user"] },
|
|
19
|
+
* { name: "subscribe", label: "Subscribe to newsletter?", type: "boolean" }
|
|
20
|
+
* ]
|
|
21
|
+
* });
|
|
22
|
+
* // values: { name: string, age: number, role: string, subscribe: boolean }
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
async function form(config) {
|
|
26
|
+
if (isSandbox()) return sendIPCRequest("prompt:form", config);
|
|
27
|
+
const result = {};
|
|
28
|
+
if (config.title) console.log(`\n${pc.bold(config.title)}\n`);
|
|
29
|
+
if (!isTTY()) {
|
|
30
|
+
if (isMCP()) {
|
|
31
|
+
const requiredFields = config.fields.filter((f) => f.required && f.defaultValue === void 0).map((f) => f.name);
|
|
32
|
+
if (requiredFields.length > 0) throw new Error(`Interactive form not available in MCP mode. All parameters must be defined in the tool's inputSchema. Missing required fields: ${requiredFields.join(", ")}`);
|
|
33
|
+
}
|
|
34
|
+
for (const field of config.fields) result[field.name] = field.defaultValue;
|
|
35
|
+
return result;
|
|
36
|
+
}
|
|
37
|
+
for (const field of config.fields) {
|
|
38
|
+
const label = field.description ? `${field.label} (${field.description})` : field.label;
|
|
39
|
+
if (field.type === "boolean") {
|
|
40
|
+
const value = await p.confirm({
|
|
41
|
+
message: label,
|
|
42
|
+
initialValue: field.defaultValue
|
|
43
|
+
});
|
|
44
|
+
if (p.isCancel(value)) {
|
|
45
|
+
p.cancel("Operation cancelled");
|
|
46
|
+
process.exit(0);
|
|
47
|
+
}
|
|
48
|
+
result[field.name] = value;
|
|
49
|
+
} else if (field.type === "enum" && field.enumValues?.length) {
|
|
50
|
+
const value = await p.select({
|
|
51
|
+
message: label,
|
|
52
|
+
options: field.enumValues.map((v) => ({
|
|
53
|
+
label: v,
|
|
54
|
+
value: v
|
|
55
|
+
}))
|
|
56
|
+
});
|
|
57
|
+
if (p.isCancel(value)) {
|
|
58
|
+
p.cancel("Operation cancelled");
|
|
59
|
+
process.exit(0);
|
|
60
|
+
}
|
|
61
|
+
result[field.name] = value;
|
|
62
|
+
} else if (field.type === "number") {
|
|
63
|
+
const strValue = await p.text({
|
|
64
|
+
message: label,
|
|
65
|
+
defaultValue: field.defaultValue?.toString(),
|
|
66
|
+
validate: (value) => {
|
|
67
|
+
if (value && Number.isNaN(Number.parseFloat(value))) return "Please enter a valid number";
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
if (p.isCancel(strValue)) {
|
|
71
|
+
p.cancel("Operation cancelled");
|
|
72
|
+
process.exit(0);
|
|
73
|
+
}
|
|
74
|
+
result[field.name] = Number.parseFloat(strValue);
|
|
75
|
+
} else {
|
|
76
|
+
const value = await p.text({
|
|
77
|
+
message: label,
|
|
78
|
+
defaultValue: field.defaultValue
|
|
79
|
+
});
|
|
80
|
+
if (p.isCancel(value)) {
|
|
81
|
+
p.cancel("Operation cancelled");
|
|
82
|
+
process.exit(0);
|
|
83
|
+
}
|
|
84
|
+
result[field.name] = value;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return result;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
//#endregion
|
|
91
|
+
export { form };
|
|
92
|
+
//# sourceMappingURL=form.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"form.mjs","names":["result: Record<string, unknown>"],"sources":["../../../src/ui/components/form.ts"],"sourcesContent":["import * as p from \"@clack/prompts\";\nimport pc from \"picocolors\";\nimport { sendIPCRequest } from \"../../sandbox/ipc-client\";\nimport { isMCP, isSandbox } from \"../../shared/runtime-mode\";\nimport { isTTY } from \"../utils/tty\";\n\n/**\n * Field definition for forms\n */\nexport interface FormField {\n /** Field name/key */\n name: string;\n /** Display label */\n label: string;\n /** Field type */\n type: \"string\" | \"number\" | \"boolean\" | \"enum\";\n /** Whether field is required */\n required?: boolean;\n /** Default value */\n defaultValue?: unknown;\n /** Description/help text */\n description?: string;\n /** Enum options (for type: \"enum\") */\n enumValues?: string[];\n}\n\n/**\n * Configuration for form component\n */\nexport interface FormConfig {\n /** Form fields */\n fields: FormField[];\n /** Form title */\n title?: string;\n}\n\n/**\n * Prompt for multiple fields sequentially\n *\n * @example\n * ```typescript\n * const values = await form({\n * title: \"User Registration\",\n * fields: [\n * { name: \"name\", label: \"Your name\", type: \"string\", required: true },\n * { name: \"age\", label: \"Your age\", type: \"number\", defaultValue: 18 },\n * { name: \"role\", label: \"Role\", type: \"enum\", enumValues: [\"admin\", \"user\"] },\n * { name: \"subscribe\", label: \"Subscribe to newsletter?\", type: \"boolean\" }\n * ]\n * });\n * // values: { name: string, age: number, role: string, subscribe: boolean }\n * ```\n */\nexport async function form(\n config: FormConfig,\n): Promise<Record<string, unknown>> {\n // Sandbox mode: use IPC to request form from host\n if (isSandbox()) {\n return sendIPCRequest<Record<string, unknown>>(\"prompt:form\", config);\n }\n\n const result: Record<string, unknown> = {};\n\n if (config.title) {\n console.log(`\\n${pc.bold(config.title)}\\n`);\n }\n\n // Non-TTY fallback: return defaults or throw in MCP mode\n if (!isTTY()) {\n // In MCP mode, interactive forms are not allowed\n if (isMCP()) {\n const requiredFields = config.fields\n .filter((f) => f.required && f.defaultValue === undefined)\n .map((f) => f.name);\n\n if (requiredFields.length > 0) {\n throw new Error(\n `Interactive form not available in MCP mode. All parameters must be defined in the tool's inputSchema. Missing required fields: ${requiredFields.join(\", \")}`,\n );\n }\n }\n\n for (const field of config.fields) {\n result[field.name] = field.defaultValue;\n }\n return result;\n }\n\n for (const field of config.fields) {\n const label = field.description\n ? `${field.label} (${field.description})`\n : field.label;\n\n if (field.type === \"boolean\") {\n const value = await p.confirm({\n message: label,\n initialValue: field.defaultValue as boolean | undefined,\n });\n\n if (p.isCancel(value)) {\n p.cancel(\"Operation cancelled\");\n process.exit(0);\n }\n\n result[field.name] = value;\n } else if (field.type === \"enum\" && field.enumValues?.length) {\n const value = await p.select({\n message: label,\n options: field.enumValues.map((v) => ({\n label: v,\n value: v,\n })),\n });\n\n if (p.isCancel(value)) {\n p.cancel(\"Operation cancelled\");\n process.exit(0);\n }\n\n result[field.name] = value;\n } else if (field.type === \"number\") {\n const strValue = await p.text({\n message: label,\n defaultValue: field.defaultValue?.toString(),\n validate: (value) => {\n if (value && Number.isNaN(Number.parseFloat(value))) {\n return \"Please enter a valid number\";\n }\n return undefined;\n },\n });\n\n if (p.isCancel(strValue)) {\n p.cancel(\"Operation cancelled\");\n process.exit(0);\n }\n\n result[field.name] = Number.parseFloat(strValue);\n } else {\n const value = await p.text({\n message: label,\n defaultValue: field.defaultValue as string | undefined,\n });\n\n if (p.isCancel(value)) {\n p.cancel(\"Operation cancelled\");\n process.exit(0);\n }\n\n result[field.name] = value;\n }\n }\n\n return result;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAqDA,eAAsB,KACpB,QACkC;AAElC,KAAI,WAAW,CACb,QAAO,eAAwC,eAAe,OAAO;CAGvE,MAAMA,SAAkC,EAAE;AAE1C,KAAI,OAAO,MACT,SAAQ,IAAI,KAAK,GAAG,KAAK,OAAO,MAAM,CAAC,IAAI;AAI7C,KAAI,CAAC,OAAO,EAAE;AAEZ,MAAI,OAAO,EAAE;GACX,MAAM,iBAAiB,OAAO,OAC3B,QAAQ,MAAM,EAAE,YAAY,EAAE,iBAAiB,OAAU,CACzD,KAAK,MAAM,EAAE,KAAK;AAErB,OAAI,eAAe,SAAS,EAC1B,OAAM,IAAI,MACR,kIAAkI,eAAe,KAAK,KAAK,GAC5J;;AAIL,OAAK,MAAM,SAAS,OAAO,OACzB,QAAO,MAAM,QAAQ,MAAM;AAE7B,SAAO;;AAGT,MAAK,MAAM,SAAS,OAAO,QAAQ;EACjC,MAAM,QAAQ,MAAM,cAChB,GAAG,MAAM,MAAM,IAAI,MAAM,YAAY,KACrC,MAAM;AAEV,MAAI,MAAM,SAAS,WAAW;GAC5B,MAAM,QAAQ,MAAM,EAAE,QAAQ;IAC5B,SAAS;IACT,cAAc,MAAM;IACrB,CAAC;AAEF,OAAI,EAAE,SAAS,MAAM,EAAE;AACrB,MAAE,OAAO,sBAAsB;AAC/B,YAAQ,KAAK,EAAE;;AAGjB,UAAO,MAAM,QAAQ;aACZ,MAAM,SAAS,UAAU,MAAM,YAAY,QAAQ;GAC5D,MAAM,QAAQ,MAAM,EAAE,OAAO;IAC3B,SAAS;IACT,SAAS,MAAM,WAAW,KAAK,OAAO;KACpC,OAAO;KACP,OAAO;KACR,EAAE;IACJ,CAAC;AAEF,OAAI,EAAE,SAAS,MAAM,EAAE;AACrB,MAAE,OAAO,sBAAsB;AAC/B,YAAQ,KAAK,EAAE;;AAGjB,UAAO,MAAM,QAAQ;aACZ,MAAM,SAAS,UAAU;GAClC,MAAM,WAAW,MAAM,EAAE,KAAK;IAC5B,SAAS;IACT,cAAc,MAAM,cAAc,UAAU;IAC5C,WAAW,UAAU;AACnB,SAAI,SAAS,OAAO,MAAM,OAAO,WAAW,MAAM,CAAC,CACjD,QAAO;;IAIZ,CAAC;AAEF,OAAI,EAAE,SAAS,SAAS,EAAE;AACxB,MAAE,OAAO,sBAAsB;AAC/B,YAAQ,KAAK,EAAE;;AAGjB,UAAO,MAAM,QAAQ,OAAO,WAAW,SAAS;SAC3C;GACL,MAAM,QAAQ,MAAM,EAAE,KAAK;IACzB,SAAS;IACT,cAAc,MAAM;IACrB,CAAC;AAEF,OAAI,EAAE,SAAS,MAAM,EAAE;AACrB,MAAE,OAAO,sBAAsB;AAC/B,YAAQ,KAAK,EAAE;;AAGjB,UAAO,MAAM,QAAQ;;;AAIzB,QAAO"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
//#region src/ui/components/input.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Configuration for input component
|
|
4
|
+
*/
|
|
5
|
+
interface InputConfig {
|
|
6
|
+
/** Prompt message */
|
|
7
|
+
prompt: string;
|
|
8
|
+
/** Default value */
|
|
9
|
+
defaultValue?: string;
|
|
10
|
+
/** Placeholder text */
|
|
11
|
+
placeholder?: string;
|
|
12
|
+
/** Maximum input length */
|
|
13
|
+
maxLength?: number;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Prompt for text input
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```typescript
|
|
20
|
+
* const name = await input({
|
|
21
|
+
* prompt: "What's your name?",
|
|
22
|
+
* defaultValue: "Anonymous"
|
|
23
|
+
* });
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
declare function input(config: InputConfig): Promise<string>;
|
|
27
|
+
//#endregion
|
|
28
|
+
export { InputConfig, input };
|
|
29
|
+
//# sourceMappingURL=input.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"input.d.mts","names":[],"sources":["../../../src/ui/components/input.ts"],"sourcesContent":[],"mappings":";;AAQA;AAsBA;UAtBiB,WAAA;;;;;;;;;;;;;;;;;;;;;iBAsBK,KAAA,SAAc,cAAc"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { isMCP, isSandbox } from "../../shared/runtime-mode.mjs";
|
|
2
|
+
import { isTTY } from "../utils/tty.mjs";
|
|
3
|
+
import { sendIPCRequest } from "../../sandbox/ipc-client.mjs";
|
|
4
|
+
import * as p from "@clack/prompts";
|
|
5
|
+
|
|
6
|
+
//#region src/ui/components/input.ts
|
|
7
|
+
/**
|
|
8
|
+
* Prompt for text input
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* const name = await input({
|
|
13
|
+
* prompt: "What's your name?",
|
|
14
|
+
* defaultValue: "Anonymous"
|
|
15
|
+
* });
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
async function input(config) {
|
|
19
|
+
if (isSandbox()) return sendIPCRequest("prompt:input", config);
|
|
20
|
+
if (!isTTY()) {
|
|
21
|
+
if (config.defaultValue !== void 0) return config.defaultValue;
|
|
22
|
+
if (isMCP()) throw new Error(`Interactive input not available in MCP mode. All parameters must be defined in the tool's inputSchema. Missing parameter: ${config.prompt}`);
|
|
23
|
+
throw new Error("Interactive input not available in non-TTY mode. Please provide all required arguments.");
|
|
24
|
+
}
|
|
25
|
+
const result = await p.text({
|
|
26
|
+
message: config.prompt,
|
|
27
|
+
defaultValue: config.defaultValue,
|
|
28
|
+
placeholder: config.placeholder,
|
|
29
|
+
validate: config.maxLength ? (value) => {
|
|
30
|
+
if (value && value.length > config.maxLength) return `Input must be ${config.maxLength} characters or less`;
|
|
31
|
+
} : void 0
|
|
32
|
+
});
|
|
33
|
+
if (p.isCancel(result)) {
|
|
34
|
+
p.cancel("Operation cancelled");
|
|
35
|
+
process.exit(0);
|
|
36
|
+
}
|
|
37
|
+
return result;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
//#endregion
|
|
41
|
+
export { input };
|
|
42
|
+
//# sourceMappingURL=input.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"input.mjs","names":[],"sources":["../../../src/ui/components/input.ts"],"sourcesContent":["import * as p from \"@clack/prompts\";\nimport { sendIPCRequest } from \"../../sandbox/ipc-client\";\nimport { isMCP, isSandbox } from \"../../shared/runtime-mode\";\nimport { isTTY } from \"../utils/tty\";\n\n/**\n * Configuration for input component\n */\nexport interface InputConfig {\n /** Prompt message */\n prompt: string;\n /** Default value */\n defaultValue?: string;\n /** Placeholder text */\n placeholder?: string;\n /** Maximum input length */\n maxLength?: number;\n}\n\n/**\n * Prompt for text input\n *\n * @example\n * ```typescript\n * const name = await input({\n * prompt: \"What's your name?\",\n * defaultValue: \"Anonymous\"\n * });\n * ```\n */\nexport async function input(config: InputConfig): Promise<string> {\n // Sandbox mode: use IPC to request input from host\n if (isSandbox()) {\n return sendIPCRequest<string>(\"prompt:input\", config);\n }\n\n // Non-TTY fallback: return default or throw\n if (!isTTY()) {\n if (config.defaultValue !== undefined) {\n return config.defaultValue;\n }\n\n // Provide MCP-specific error message\n if (isMCP()) {\n throw new Error(\n `Interactive input not available in MCP mode. All parameters must be defined in the tool's inputSchema. Missing parameter: ${config.prompt}`,\n );\n }\n\n throw new Error(\n \"Interactive input not available in non-TTY mode. Please provide all required arguments.\",\n );\n }\n\n const result = await p.text({\n message: config.prompt,\n defaultValue: config.defaultValue,\n placeholder: config.placeholder,\n validate: config.maxLength\n ? (value) => {\n if (value && value.length > config.maxLength!) {\n return `Input must be ${config.maxLength} characters or less`;\n }\n return undefined;\n }\n : undefined,\n });\n\n if (p.isCancel(result)) {\n p.cancel(\"Operation cancelled\");\n process.exit(0);\n }\n\n return result;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AA8BA,eAAsB,MAAM,QAAsC;AAEhE,KAAI,WAAW,CACb,QAAO,eAAuB,gBAAgB,OAAO;AAIvD,KAAI,CAAC,OAAO,EAAE;AACZ,MAAI,OAAO,iBAAiB,OAC1B,QAAO,OAAO;AAIhB,MAAI,OAAO,CACT,OAAM,IAAI,MACR,6HAA6H,OAAO,SACrI;AAGH,QAAM,IAAI,MACR,0FACD;;CAGH,MAAM,SAAS,MAAM,EAAE,KAAK;EAC1B,SAAS,OAAO;EAChB,cAAc,OAAO;EACrB,aAAa,OAAO;EACpB,UAAU,OAAO,aACZ,UAAU;AACT,OAAI,SAAS,MAAM,SAAS,OAAO,UACjC,QAAO,iBAAiB,OAAO,UAAU;MAI7C;EACL,CAAC;AAEF,KAAI,EAAE,SAAS,OAAO,EAAE;AACtB,IAAE,OAAO,sBAAsB;AAC/B,UAAQ,KAAK,EAAE;;AAGjB,QAAO"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
//#region src/ui/components/select.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Option for select menus
|
|
4
|
+
*/
|
|
5
|
+
interface SelectOption<T = string> {
|
|
6
|
+
/** Display name */
|
|
7
|
+
name: string;
|
|
8
|
+
/** Optional description shown below name */
|
|
9
|
+
description?: string;
|
|
10
|
+
/** Value returned when selected */
|
|
11
|
+
value: T;
|
|
12
|
+
/** Disable this option */
|
|
13
|
+
disabled?: boolean;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Configuration for select component
|
|
17
|
+
*/
|
|
18
|
+
interface SelectConfig<T> {
|
|
19
|
+
/** Options to choose from */
|
|
20
|
+
options: SelectOption<T>[];
|
|
21
|
+
/** Prompt message */
|
|
22
|
+
prompt?: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Show a selection menu and wait for user choice
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```typescript
|
|
29
|
+
* const color = await select({
|
|
30
|
+
* options: [
|
|
31
|
+
* { name: "Red", value: "red" },
|
|
32
|
+
* { name: "Blue", value: "blue", description: "Ocean color" },
|
|
33
|
+
* ],
|
|
34
|
+
* prompt: "Pick a color"
|
|
35
|
+
* });
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
declare function select<T = string>(config: SelectConfig<T>): Promise<T>;
|
|
39
|
+
//#endregion
|
|
40
|
+
export { SelectConfig, SelectOption, select };
|
|
41
|
+
//# sourceMappingURL=select.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"select.d.mts","names":[],"sources":["../../../src/ui/components/select.ts"],"sourcesContent":[],"mappings":";;AAQA;AAcA;AAqBsB,UAnCL,YAmCW,CAAA,IAAA,MAAA,CAAA,CAAA;EAAkC;EAAb,IAAA,EAAA,MAAA;EAA0B;EAAR,WAAA,CAAA,EAAA,MAAA;EAAO;SA7BjE;;;;;;;UAQQ;;WAEN,aAAa;;;;;;;;;;;;;;;;;;iBAmBF,2BAA2B,aAAa,KAAK,QAAQ"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { isMCP, isSandbox } from "../../shared/runtime-mode.mjs";
|
|
2
|
+
import { isTTY } from "../utils/tty.mjs";
|
|
3
|
+
import { sendIPCRequest } from "../../sandbox/ipc-client.mjs";
|
|
4
|
+
import * as p from "@clack/prompts";
|
|
5
|
+
|
|
6
|
+
//#region src/ui/components/select.ts
|
|
7
|
+
/**
|
|
8
|
+
* Show a selection menu and wait for user choice
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* const color = await select({
|
|
13
|
+
* options: [
|
|
14
|
+
* { name: "Red", value: "red" },
|
|
15
|
+
* { name: "Blue", value: "blue", description: "Ocean color" },
|
|
16
|
+
* ],
|
|
17
|
+
* prompt: "Pick a color"
|
|
18
|
+
* });
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
async function select(config) {
|
|
22
|
+
if (isSandbox()) return sendIPCRequest("prompt:select", {
|
|
23
|
+
prompt: config.prompt ?? "Select an option",
|
|
24
|
+
options: config.options
|
|
25
|
+
});
|
|
26
|
+
if (!isTTY()) {
|
|
27
|
+
if (isMCP()) throw new Error(`Interactive selection not available in MCP mode. All parameters must be defined in the tool's inputSchema. Selection prompt: ${config.prompt}`);
|
|
28
|
+
const firstOption = config.options[0];
|
|
29
|
+
if (!firstOption) throw new Error("No options provided");
|
|
30
|
+
return firstOption.value;
|
|
31
|
+
}
|
|
32
|
+
const mappedOptions = config.options.map((opt) => ({
|
|
33
|
+
label: opt.name,
|
|
34
|
+
value: opt.value,
|
|
35
|
+
...opt.description && { hint: opt.description }
|
|
36
|
+
}));
|
|
37
|
+
const result = await p.select({
|
|
38
|
+
message: config.prompt ?? "Select an option",
|
|
39
|
+
options: mappedOptions
|
|
40
|
+
});
|
|
41
|
+
if (p.isCancel(result)) {
|
|
42
|
+
p.cancel("Operation cancelled");
|
|
43
|
+
process.exit(0);
|
|
44
|
+
}
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
//#endregion
|
|
49
|
+
export { select };
|
|
50
|
+
//# sourceMappingURL=select.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"select.mjs","names":[],"sources":["../../../src/ui/components/select.ts"],"sourcesContent":["import * as p from \"@clack/prompts\";\nimport { sendIPCRequest } from \"../../sandbox/ipc-client\";\nimport { isMCP, isSandbox } from \"../../shared/runtime-mode\";\nimport { isTTY } from \"../utils/tty\";\n\n/**\n * Option for select menus\n */\nexport interface SelectOption<T = string> {\n /** Display name */\n name: string;\n /** Optional description shown below name */\n description?: string;\n /** Value returned when selected */\n value: T;\n /** Disable this option */\n disabled?: boolean;\n}\n\n/**\n * Configuration for select component\n */\nexport interface SelectConfig<T> {\n /** Options to choose from */\n options: SelectOption<T>[];\n /** Prompt message */\n prompt?: string;\n}\n\n/**\n * Show a selection menu and wait for user choice\n *\n * @example\n * ```typescript\n * const color = await select({\n * options: [\n * { name: \"Red\", value: \"red\" },\n * { name: \"Blue\", value: \"blue\", description: \"Ocean color\" },\n * ],\n * prompt: \"Pick a color\"\n * });\n * ```\n */\nexport async function select<T = string>(config: SelectConfig<T>): Promise<T> {\n // Sandbox mode: use IPC to request select from host\n if (isSandbox()) {\n return sendIPCRequest<T>(\"prompt:select\", {\n prompt: config.prompt ?? \"Select an option\",\n options: config.options,\n });\n }\n\n // Non-TTY fallback: auto-select first option or throw in MCP mode\n if (!isTTY()) {\n // In MCP mode, interactive selection is not allowed\n if (isMCP()) {\n throw new Error(\n `Interactive selection not available in MCP mode. All parameters must be defined in the tool's inputSchema. Selection prompt: ${config.prompt}`,\n );\n }\n\n const firstOption = config.options[0];\n if (!firstOption) {\n throw new Error(\"No options provided\");\n }\n return firstOption.value;\n }\n\n // Map to @clack/prompts Option format\n // Using type assertion due to complex Option<T> conditional types\n const mappedOptions = config.options.map((opt) => ({\n label: opt.name,\n value: opt.value as unknown,\n ...(opt.description && { hint: opt.description }),\n }));\n\n const result = await p.select({\n message: config.prompt ?? \"Select an option\",\n options: mappedOptions,\n });\n\n if (p.isCancel(result)) {\n p.cancel(\"Operation cancelled\");\n process.exit(0);\n }\n\n return result as T;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AA2CA,eAAsB,OAAmB,QAAqC;AAE5E,KAAI,WAAW,CACb,QAAO,eAAkB,iBAAiB;EACxC,QAAQ,OAAO,UAAU;EACzB,SAAS,OAAO;EACjB,CAAC;AAIJ,KAAI,CAAC,OAAO,EAAE;AAEZ,MAAI,OAAO,CACT,OAAM,IAAI,MACR,gIAAgI,OAAO,SACxI;EAGH,MAAM,cAAc,OAAO,QAAQ;AACnC,MAAI,CAAC,YACH,OAAM,IAAI,MAAM,sBAAsB;AAExC,SAAO,YAAY;;CAKrB,MAAM,gBAAgB,OAAO,QAAQ,KAAK,SAAS;EACjD,OAAO,IAAI;EACX,OAAO,IAAI;EACX,GAAI,IAAI,eAAe,EAAE,MAAM,IAAI,aAAa;EACjD,EAAE;CAEH,MAAM,SAAS,MAAM,EAAE,OAAO;EAC5B,SAAS,OAAO,UAAU;EAC1B,SAAS;EACV,CAAC;AAEF,KAAI,EAAE,SAAS,OAAO,EAAE;AACtB,IAAE,OAAO,sBAAsB;AAC/B,UAAQ,KAAK,EAAE;;AAGjB,QAAO"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
//#region src/ui/components/spinner.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Handle for controlling a spinner
|
|
4
|
+
*/
|
|
5
|
+
interface SpinnerHandle {
|
|
6
|
+
/** Update spinner message */
|
|
7
|
+
update(message: string): void;
|
|
8
|
+
/** Mark as successful and stop */
|
|
9
|
+
succeed(message?: string): void;
|
|
10
|
+
/** Mark as failed and stop */
|
|
11
|
+
fail(message?: string): void;
|
|
12
|
+
/** Stop spinner without status */
|
|
13
|
+
stop(): void;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Create a spinner for showing progress
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```typescript
|
|
20
|
+
* const spin = spinner("Loading...");
|
|
21
|
+
* await doSomething();
|
|
22
|
+
* spin.succeed("Done!");
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
declare function spinner(message: string): SpinnerHandle;
|
|
26
|
+
//#endregion
|
|
27
|
+
export { SpinnerHandle, spinner };
|
|
28
|
+
//# sourceMappingURL=spinner.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spinner.d.mts","names":[],"sources":["../../../src/ui/components/spinner.ts"],"sourcesContent":[],"mappings":";;AAKA;AAqBA;UArBiB,aAAA;;;;;;;;;;;;;;;;;;;;iBAqBD,OAAA,mBAA0B"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import * as p from "@clack/prompts";
|
|
2
|
+
|
|
3
|
+
//#region src/ui/components/spinner.ts
|
|
4
|
+
/**
|
|
5
|
+
* Create a spinner for showing progress
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* const spin = spinner("Loading...");
|
|
10
|
+
* await doSomething();
|
|
11
|
+
* spin.succeed("Done!");
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
function spinner(message) {
|
|
15
|
+
const instance = p.spinner();
|
|
16
|
+
instance.start(message);
|
|
17
|
+
return {
|
|
18
|
+
update(msg) {
|
|
19
|
+
instance.message(msg);
|
|
20
|
+
},
|
|
21
|
+
succeed(msg) {
|
|
22
|
+
instance.stop(msg ?? message);
|
|
23
|
+
},
|
|
24
|
+
fail(msg) {
|
|
25
|
+
instance.stop(msg ?? message);
|
|
26
|
+
},
|
|
27
|
+
stop() {
|
|
28
|
+
instance.stop();
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
//#endregion
|
|
34
|
+
export { spinner };
|
|
35
|
+
//# sourceMappingURL=spinner.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spinner.mjs","names":[],"sources":["../../../src/ui/components/spinner.ts"],"sourcesContent":["import * as p from \"@clack/prompts\";\n\n/**\n * Handle for controlling a spinner\n */\nexport interface SpinnerHandle {\n /** Update spinner message */\n update(message: string): void;\n /** Mark as successful and stop */\n succeed(message?: string): void;\n /** Mark as failed and stop */\n fail(message?: string): void;\n /** Stop spinner without status */\n stop(): void;\n}\n\n/**\n * Create a spinner for showing progress\n *\n * @example\n * ```typescript\n * const spin = spinner(\"Loading...\");\n * await doSomething();\n * spin.succeed(\"Done!\");\n * ```\n */\nexport function spinner(message: string): SpinnerHandle {\n const instance = p.spinner();\n instance.start(message);\n\n return {\n update(msg: string) {\n instance.message(msg);\n },\n\n succeed(msg?: string) {\n instance.stop(msg ?? message);\n },\n\n fail(msg?: string) {\n instance.stop(msg ?? message);\n },\n\n stop() {\n instance.stop();\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;AA0BA,SAAgB,QAAQ,SAAgC;CACtD,MAAM,WAAW,EAAE,SAAS;AAC5B,UAAS,MAAM,QAAQ;AAEvB,QAAO;EACL,OAAO,KAAa;AAClB,YAAS,QAAQ,IAAI;;EAGvB,QAAQ,KAAc;AACpB,YAAS,KAAK,OAAO,QAAQ;;EAG/B,KAAK,KAAc;AACjB,YAAS,KAAK,OAAO,QAAQ;;EAG/B,OAAO;AACL,YAAS,MAAM;;EAElB"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
//#region src/ui/components/table.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Column alignment options
|
|
4
|
+
*/
|
|
5
|
+
type ColumnAlign = "left" | "center" | "right";
|
|
6
|
+
/**
|
|
7
|
+
* Column definition for table
|
|
8
|
+
*/
|
|
9
|
+
interface TableColumn<T = Record<string, unknown>> {
|
|
10
|
+
/** Column key in the data object */
|
|
11
|
+
key: keyof T;
|
|
12
|
+
/** Display header text */
|
|
13
|
+
header: string;
|
|
14
|
+
/** Column alignment (default: left) */
|
|
15
|
+
align?: ColumnAlign;
|
|
16
|
+
/** Fixed column width (auto-calculated if not specified) */
|
|
17
|
+
width?: number;
|
|
18
|
+
/** Custom formatter function for cell values */
|
|
19
|
+
formatter?: (value: unknown, row: T) => string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Configuration for table component
|
|
23
|
+
*/
|
|
24
|
+
interface TableConfig<T = Record<string, unknown>> {
|
|
25
|
+
/** Column definitions */
|
|
26
|
+
columns: TableColumn<T>[];
|
|
27
|
+
/** Data rows */
|
|
28
|
+
rows: T[];
|
|
29
|
+
/** Show header row (default: true) */
|
|
30
|
+
showHeader?: boolean;
|
|
31
|
+
/** Show borders (default: true in TTY mode) */
|
|
32
|
+
showBorders?: boolean;
|
|
33
|
+
/** Table title (optional) */
|
|
34
|
+
title?: string;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Display a table with columns and rows
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```typescript
|
|
41
|
+
* table({
|
|
42
|
+
* title: "Users",
|
|
43
|
+
* columns: [
|
|
44
|
+
* { key: "name", header: "Name" },
|
|
45
|
+
* { key: "age", header: "Age", align: "right" },
|
|
46
|
+
* { key: "status", header: "Status", formatter: (val) =>
|
|
47
|
+
* val === "active" ? pc.green("✓ Active") : pc.red("✗ Inactive")
|
|
48
|
+
* },
|
|
49
|
+
* ],
|
|
50
|
+
* rows: [
|
|
51
|
+
* { name: "Alice", age: 25, status: "active" },
|
|
52
|
+
* { name: "Bob", age: 30, status: "inactive" },
|
|
53
|
+
* ],
|
|
54
|
+
* });
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
declare function table<T = Record<string, unknown>>(config: TableConfig<T>): void;
|
|
58
|
+
//#endregion
|
|
59
|
+
export { ColumnAlign, TableColumn, TableConfig, table };
|
|
60
|
+
//# sourceMappingURL=table.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"table.d.mts","names":[],"sources":["../../../src/ui/components/table.ts"],"sourcesContent":[],"mappings":";;AAMA;AAKA;AAAiC,KALrB,WAAA,GAKqB,MAAA,GAAA,QAAA,GAAA,OAAA;;;;AAUI,UAVpB,WAUoB,CAAA,IAVJ,MAUI,CAAA,MAAA,EAAA,OAAA,CAAA,CAAA,CAAA;EAMpB;EAAgB,GAAA,EAAA,MAdpB,CAcoB;EAEV;EAAZ,MAAA,EAAA,MAAA;EAEH;EAAC,KAAA,CAAA,EAdC,WAcD;EA4MO;EAAU,KAAA,CAAA,EAAA,MAAA;EACJ;EAAZ,SAAA,CAAA,EAAA,CAAA,KAAA,EAAA,OAAA,EAAA,GAAA,EAvN0B,CAuN1B,EAAA,GAAA,MAAA;;;;;UAjNO,gBAAgB;;WAEtB,YAAY;;QAEf;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA4MQ,UAAU,iCAChB,YAAY"}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { isTTY } from "../utils/tty.mjs";
|
|
2
|
+
import { formatText, pc } from "../utils/colors.mjs";
|
|
3
|
+
|
|
4
|
+
//#region src/ui/components/table.ts
|
|
5
|
+
/**
|
|
6
|
+
* Align text within a given width
|
|
7
|
+
*/
|
|
8
|
+
function alignText(text, width, align) {
|
|
9
|
+
const textLength = stripAnsi(text).length;
|
|
10
|
+
const padding = Math.max(0, width - textLength);
|
|
11
|
+
switch (align) {
|
|
12
|
+
case "right": return " ".repeat(padding) + text;
|
|
13
|
+
case "center": {
|
|
14
|
+
const leftPad = Math.floor(padding / 2);
|
|
15
|
+
const rightPad = padding - leftPad;
|
|
16
|
+
return " ".repeat(leftPad) + text + " ".repeat(rightPad);
|
|
17
|
+
}
|
|
18
|
+
default: return text + " ".repeat(padding);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Strip ANSI escape codes from string for length calculation
|
|
23
|
+
*/
|
|
24
|
+
function stripAnsi(str) {
|
|
25
|
+
return str.replace(/\x1b\[[0-9;]*m/g, "");
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Calculate column widths based on content
|
|
29
|
+
*/
|
|
30
|
+
function calculateColumnWidths(columns, rows, showHeader) {
|
|
31
|
+
return columns.map((col) => {
|
|
32
|
+
if (col.width !== void 0) return col.width;
|
|
33
|
+
let maxWidth = showHeader ? col.header.length : 0;
|
|
34
|
+
for (const row of rows) {
|
|
35
|
+
const value = row[col.key];
|
|
36
|
+
const length = stripAnsi(col.formatter ? col.formatter(value, row) : String(value ?? "")).length;
|
|
37
|
+
maxWidth = Math.max(maxWidth, length);
|
|
38
|
+
}
|
|
39
|
+
return maxWidth;
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Format a single cell value
|
|
44
|
+
*/
|
|
45
|
+
function formatCell(value, row, column) {
|
|
46
|
+
if (column.formatter) return column.formatter(value, row);
|
|
47
|
+
if (value === null || value === void 0) return pc.dim("-");
|
|
48
|
+
return String(value);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Render table in TTY mode with borders and styling
|
|
52
|
+
*/
|
|
53
|
+
function renderTTY(config) {
|
|
54
|
+
const { columns, rows, showHeader = true, showBorders = true, title } = config;
|
|
55
|
+
const lines = [];
|
|
56
|
+
const widths = calculateColumnWidths(columns, rows, showHeader);
|
|
57
|
+
if (title) {
|
|
58
|
+
lines.push("");
|
|
59
|
+
lines.push(formatText(`${title}`, { bold: true }));
|
|
60
|
+
lines.push("");
|
|
61
|
+
}
|
|
62
|
+
if (showHeader) {
|
|
63
|
+
const headerCells = columns.map((col, i) => {
|
|
64
|
+
return alignText(formatText(col.header, {
|
|
65
|
+
bold: true,
|
|
66
|
+
color: "cyan"
|
|
67
|
+
}), widths[i], col.align ?? "left");
|
|
68
|
+
});
|
|
69
|
+
lines.push(headerCells.join(" "));
|
|
70
|
+
if (showBorders) {
|
|
71
|
+
const separatorParts = widths.map((w) => "─".repeat(w));
|
|
72
|
+
lines.push(pc.gray(separatorParts.join("─")));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
for (const row of rows) {
|
|
76
|
+
const cells = columns.map((col, i) => {
|
|
77
|
+
const value = row[col.key];
|
|
78
|
+
return alignText(formatCell(value, row, col), widths[i], col.align ?? "left");
|
|
79
|
+
});
|
|
80
|
+
lines.push(cells.join(" "));
|
|
81
|
+
}
|
|
82
|
+
if (showBorders && rows.length > 0) {
|
|
83
|
+
const separatorParts = widths.map((w) => "─".repeat(w));
|
|
84
|
+
lines.push(pc.gray(separatorParts.join("─")));
|
|
85
|
+
}
|
|
86
|
+
return lines.join("\n");
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Render table in non-TTY mode (plain text)
|
|
90
|
+
*/
|
|
91
|
+
function renderPlain(config) {
|
|
92
|
+
const { columns, rows, showHeader = true, title } = config;
|
|
93
|
+
const lines = [];
|
|
94
|
+
const widths = calculateColumnWidths(columns, rows, showHeader);
|
|
95
|
+
if (title) {
|
|
96
|
+
lines.push("");
|
|
97
|
+
lines.push(title);
|
|
98
|
+
lines.push("");
|
|
99
|
+
}
|
|
100
|
+
if (showHeader) {
|
|
101
|
+
const headerCells = columns.map((col, i) => alignText(col.header, widths[i], col.align ?? "left"));
|
|
102
|
+
lines.push(headerCells.join(" "));
|
|
103
|
+
const separator = columns.map((_, i) => "-".repeat(widths[i])).join(" ");
|
|
104
|
+
lines.push(separator);
|
|
105
|
+
}
|
|
106
|
+
for (const row of rows) {
|
|
107
|
+
const cells = columns.map((col, i) => {
|
|
108
|
+
const value = row[col.key];
|
|
109
|
+
return alignText(stripAnsi(formatCell(value, row, col)), widths[i], col.align ?? "left");
|
|
110
|
+
});
|
|
111
|
+
lines.push(cells.join(" "));
|
|
112
|
+
}
|
|
113
|
+
return lines.join("\n");
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Display a table with columns and rows
|
|
117
|
+
*
|
|
118
|
+
* @example
|
|
119
|
+
* ```typescript
|
|
120
|
+
* table({
|
|
121
|
+
* title: "Users",
|
|
122
|
+
* columns: [
|
|
123
|
+
* { key: "name", header: "Name" },
|
|
124
|
+
* { key: "age", header: "Age", align: "right" },
|
|
125
|
+
* { key: "status", header: "Status", formatter: (val) =>
|
|
126
|
+
* val === "active" ? pc.green("✓ Active") : pc.red("✗ Inactive")
|
|
127
|
+
* },
|
|
128
|
+
* ],
|
|
129
|
+
* rows: [
|
|
130
|
+
* { name: "Alice", age: 25, status: "active" },
|
|
131
|
+
* { name: "Bob", age: 30, status: "inactive" },
|
|
132
|
+
* ],
|
|
133
|
+
* });
|
|
134
|
+
* ```
|
|
135
|
+
*/
|
|
136
|
+
function table(config) {
|
|
137
|
+
const output = isTTY() ? renderTTY(config) : renderPlain(config);
|
|
138
|
+
console.log(output);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
//#endregion
|
|
142
|
+
export { table };
|
|
143
|
+
//# sourceMappingURL=table.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"table.mjs","names":["lines: string[]"],"sources":["../../../src/ui/components/table.ts"],"sourcesContent":["import { formatText, pc } from \"../utils/colors\";\nimport { isTTY } from \"../utils/tty\";\n\n/**\n * Column alignment options\n */\nexport type ColumnAlign = \"left\" | \"center\" | \"right\";\n\n/**\n * Column definition for table\n */\nexport interface TableColumn<T = Record<string, unknown>> {\n /** Column key in the data object */\n key: keyof T;\n /** Display header text */\n header: string;\n /** Column alignment (default: left) */\n align?: ColumnAlign;\n /** Fixed column width (auto-calculated if not specified) */\n width?: number;\n /** Custom formatter function for cell values */\n formatter?: (value: unknown, row: T) => string;\n}\n\n/**\n * Configuration for table component\n */\nexport interface TableConfig<T = Record<string, unknown>> {\n /** Column definitions */\n columns: TableColumn<T>[];\n /** Data rows */\n rows: T[];\n /** Show header row (default: true) */\n showHeader?: boolean;\n /** Show borders (default: true in TTY mode) */\n showBorders?: boolean;\n /** Table title (optional) */\n title?: string;\n}\n\n/**\n * Align text within a given width\n */\nfunction alignText(text: string, width: number, align: ColumnAlign): string {\n const textLength = stripAnsi(text).length;\n const padding = Math.max(0, width - textLength);\n\n switch (align) {\n case \"right\":\n return \" \".repeat(padding) + text;\n case \"center\": {\n const leftPad = Math.floor(padding / 2);\n const rightPad = padding - leftPad;\n return \" \".repeat(leftPad) + text + \" \".repeat(rightPad);\n }\n default:\n return text + \" \".repeat(padding);\n }\n}\n\n/**\n * Strip ANSI escape codes from string for length calculation\n */\nfunction stripAnsi(str: string): string {\n // biome-ignore lint/suspicious/noControlCharactersInRegex: ANSI codes regex\n return str.replace(/\\x1b\\[[0-9;]*m/g, \"\");\n}\n\n/**\n * Calculate column widths based on content\n */\nfunction calculateColumnWidths<T>(\n columns: TableColumn<T>[],\n rows: T[],\n showHeader: boolean,\n): number[] {\n return columns.map((col) => {\n // Use fixed width if specified\n if (col.width !== undefined) {\n return col.width;\n }\n\n // Calculate max width from header and data\n let maxWidth = showHeader ? col.header.length : 0;\n\n for (const row of rows) {\n const value = row[col.key];\n const formatted = col.formatter\n ? col.formatter(value, row)\n : String(value ?? \"\");\n const length = stripAnsi(formatted).length;\n maxWidth = Math.max(maxWidth, length);\n }\n\n return maxWidth;\n });\n}\n\n/**\n * Format a single cell value\n */\nfunction formatCell<T>(value: unknown, row: T, column: TableColumn<T>): string {\n if (column.formatter) {\n return column.formatter(value, row);\n }\n\n if (value === null || value === undefined) {\n return pc.dim(\"-\");\n }\n\n return String(value);\n}\n\n/**\n * Render table in TTY mode with borders and styling\n */\nfunction renderTTY<T>(config: TableConfig<T>): string {\n const {\n columns,\n rows,\n showHeader = true,\n showBorders = true,\n title,\n } = config;\n const lines: string[] = [];\n\n // Calculate column widths\n const widths = calculateColumnWidths(columns, rows, showHeader);\n\n // Title\n if (title) {\n lines.push(\"\");\n lines.push(formatText(`${title}`, { bold: true }));\n lines.push(\"\");\n }\n\n // Header row\n if (showHeader) {\n const headerCells = columns.map((col, i) => {\n const text = formatText(col.header, { bold: true, color: \"cyan\" });\n return alignText(text, widths[i]!, col.align ?? \"left\");\n });\n\n lines.push(headerCells.join(\" \"));\n\n // Header separator\n if (showBorders) {\n const separatorParts = widths.map((w) => \"─\".repeat(w));\n lines.push(pc.gray(separatorParts.join(\"─\")));\n }\n }\n\n // Data rows\n for (const row of rows) {\n const cells = columns.map((col, i) => {\n const value = row[col.key];\n const formatted = formatCell(value, row, col);\n return alignText(formatted, widths[i]!, col.align ?? \"left\");\n });\n\n lines.push(cells.join(\" \"));\n }\n\n // Bottom border (optional)\n if (showBorders && rows.length > 0) {\n const separatorParts = widths.map((w) => \"─\".repeat(w));\n lines.push(pc.gray(separatorParts.join(\"─\")));\n }\n\n return lines.join(\"\\n\");\n}\n\n/**\n * Render table in non-TTY mode (plain text)\n */\nfunction renderPlain<T>(config: TableConfig<T>): string {\n const { columns, rows, showHeader = true, title } = config;\n const lines: string[] = [];\n\n // Calculate column widths\n const widths = calculateColumnWidths(columns, rows, showHeader);\n\n // Title\n if (title) {\n lines.push(\"\");\n lines.push(title);\n lines.push(\"\");\n }\n\n // Header row\n if (showHeader) {\n const headerCells = columns.map((col, i) =>\n alignText(col.header, widths[i]!, col.align ?? \"left\"),\n );\n lines.push(headerCells.join(\" \"));\n\n // Separator\n const separator = columns.map((_, i) => \"-\".repeat(widths[i]!)).join(\" \");\n lines.push(separator);\n }\n\n // Data rows\n for (const row of rows) {\n const cells = columns.map((col, i) => {\n const value = row[col.key];\n const formatted = formatCell(value, row, col);\n return alignText(stripAnsi(formatted), widths[i]!, col.align ?? \"left\");\n });\n lines.push(cells.join(\" \"));\n }\n\n return lines.join(\"\\n\");\n}\n\n/**\n * Display a table with columns and rows\n *\n * @example\n * ```typescript\n * table({\n * title: \"Users\",\n * columns: [\n * { key: \"name\", header: \"Name\" },\n * { key: \"age\", header: \"Age\", align: \"right\" },\n * { key: \"status\", header: \"Status\", formatter: (val) =>\n * val === \"active\" ? pc.green(\"✓ Active\") : pc.red(\"✗ Inactive\")\n * },\n * ],\n * rows: [\n * { name: \"Alice\", age: 25, status: \"active\" },\n * { name: \"Bob\", age: 30, status: \"inactive\" },\n * ],\n * });\n * ```\n */\nexport function table<T = Record<string, unknown>>(\n config: TableConfig<T>,\n): void {\n const output = isTTY() ? renderTTY(config) : renderPlain(config);\n console.log(output);\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { confirm } from "./components/confirm.mjs";
|
|
2
|
+
import { FormConfig, FormField, form } from "./components/form.mjs";
|
|
3
|
+
import { InputConfig, input } from "./components/input.mjs";
|
|
4
|
+
import { SelectConfig, SelectOption, select } from "./components/select.mjs";
|
|
5
|
+
import { SpinnerHandle, spinner } from "./components/spinner.mjs";
|
|
6
|
+
import { ColumnAlign, TableColumn, TableConfig, table } from "./components/table.mjs";
|
|
7
|
+
import { AnsiColor, formatText, pc, theme } from "./utils/colors.mjs";
|
|
8
|
+
import { error, help, output } from "./utils/output.mjs";
|
|
9
|
+
import { isTTY } from "./utils/tty.mjs";
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import pc$1 from "picocolors";
|
|
2
|
+
|
|
3
|
+
//#region src/ui/utils/colors.d.ts
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Default color theme for UI components (hex values for reference)
|
|
7
|
+
*/
|
|
8
|
+
declare const theme: {
|
|
9
|
+
readonly primary: "#3b82f6";
|
|
10
|
+
readonly success: "#10b981";
|
|
11
|
+
readonly warning: "#f59e0b";
|
|
12
|
+
readonly error: "#ef4444";
|
|
13
|
+
readonly info: "#06b6d4";
|
|
14
|
+
readonly background: "#161b22";
|
|
15
|
+
readonly surface: "#1e293b";
|
|
16
|
+
readonly border: "#30363d";
|
|
17
|
+
readonly text: "#c9d1d9";
|
|
18
|
+
readonly textDim: "#8b949e";
|
|
19
|
+
readonly textBright: "#ffffff";
|
|
20
|
+
readonly focused: "#3b82f6";
|
|
21
|
+
readonly selected: "#3b82f6";
|
|
22
|
+
readonly hover: "#334155";
|
|
23
|
+
readonly disabled: "#6e7681";
|
|
24
|
+
};
|
|
25
|
+
type AnsiColor = "red" | "green" | "yellow" | "blue" | "magenta" | "cyan" | "white" | "gray";
|
|
26
|
+
/**
|
|
27
|
+
* Format text with picocolors
|
|
28
|
+
*/
|
|
29
|
+
declare function formatText(text: string, options?: {
|
|
30
|
+
color?: AnsiColor;
|
|
31
|
+
bold?: boolean;
|
|
32
|
+
dim?: boolean;
|
|
33
|
+
italic?: boolean;
|
|
34
|
+
underline?: boolean;
|
|
35
|
+
}): string;
|
|
36
|
+
//#endregion
|
|
37
|
+
export { AnsiColor, formatText, pc$1 as pc, theme };
|
|
38
|
+
//# sourceMappingURL=colors.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"colors.d.mts","names":[],"sources":["../../../src/ui/utils/colors.ts"],"sourcesContent":[],"mappings":";;;;;;AAKA;AAuBY,cAvBC,KAuBQ,EAAA;EAaL,SAAA,OAAU,EAAA,SAGd;;;;;;;;;;;;;;;;KAhBA,SAAA;;;;iBAaI,UAAA;UAGJ"}
|