@onexeor/lumo 0.0.3 → 0.0.4
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/package.json +2 -7
- package/src/commands/doctor.js +11 -14
- package/src/commands/init.js +23 -31
- package/src/commands/uninstall.js +13 -16
- package/src/index.js +114 -43
- package/src/lib/prompt.js +79 -0
- package/src/lib/style.js +28 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onexeor/lumo",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
4
|
"description": "Install Lumo — mobile UI/UX design intelligence for AI coding assistants (Claude Code, Cursor, Codex, MCP clients).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -40,11 +40,6 @@
|
|
|
40
40
|
"url": "https://github.com/OneXeor-Dev/lumo.git"
|
|
41
41
|
},
|
|
42
42
|
"engines": {
|
|
43
|
-
"node": ">=18"
|
|
44
|
-
},
|
|
45
|
-
"dependencies": {
|
|
46
|
-
"commander": "^12.1.0",
|
|
47
|
-
"kleur": "^4.1.5",
|
|
48
|
-
"prompts": "^2.4.2"
|
|
43
|
+
"node": ">=18.3"
|
|
49
44
|
}
|
|
50
45
|
}
|
package/src/commands/doctor.js
CHANGED
|
@@ -1,28 +1,27 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
|
|
3
|
-
import kleur from "kleur";
|
|
4
|
-
|
|
5
3
|
import { CLIENTS } from "../lib/clients.js";
|
|
6
4
|
import { LUMO_HOME, listInstalledBinaries, venvBinary } from "../lib/python.js";
|
|
5
|
+
import { bold, cyan, dim, green, red, yellow } from "../lib/style.js";
|
|
7
6
|
|
|
8
7
|
function row(ok, label, detail = "") {
|
|
9
|
-
const icon = ok ?
|
|
10
|
-
const text = ok ?
|
|
11
|
-
|
|
8
|
+
const icon = ok ? green("✓") : red("✗");
|
|
9
|
+
const text = ok ? label : yellow(label);
|
|
10
|
+
const trail = detail ? " " + dim(detail) : "";
|
|
11
|
+
process.stdout.write(` ${icon} ${text}${trail}\n`);
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
export async function doctorCommand() {
|
|
15
|
-
|
|
15
|
+
process.stdout.write("\n" + bold(cyan("• lumo doctor")) + "\n\n");
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
row(fs.existsSync(LUMO_HOME),
|
|
19
|
-
row(fs.existsSync(venvBinary("python")),
|
|
17
|
+
process.stdout.write(bold("Python tools") + "\n");
|
|
18
|
+
row(fs.existsSync(LUMO_HOME), "Lumo home", LUMO_HOME);
|
|
19
|
+
row(fs.existsSync(venvBinary("python")), "venv Python", venvBinary("python"));
|
|
20
20
|
for (const bin of listInstalledBinaries()) {
|
|
21
21
|
row(bin.exists, bin.name, bin.path);
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
console.log(kleur.bold("Client integrations"));
|
|
24
|
+
process.stdout.write("\n" + bold("Client integrations") + "\n");
|
|
26
25
|
for (const client of CLIENTS) {
|
|
27
26
|
if (!client.skillDir) continue;
|
|
28
27
|
const skillPresent = fs.existsSync(client.skillDir);
|
|
@@ -44,7 +43,5 @@ export async function doctorCommand() {
|
|
|
44
43
|
}
|
|
45
44
|
}
|
|
46
45
|
|
|
47
|
-
|
|
48
|
-
console.log(kleur.dim("Run `lumo init` to install or repair any missing pieces."));
|
|
49
|
-
console.log("");
|
|
46
|
+
process.stdout.write("\n" + dim("Run `lumo init` to install or repair any missing pieces.") + "\n\n");
|
|
50
47
|
}
|
package/src/commands/init.js
CHANGED
|
@@ -1,14 +1,12 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
1
|
import path from "node:path";
|
|
3
2
|
import { fileURLToPath } from "node:url";
|
|
4
3
|
|
|
5
|
-
import kleur from "kleur";
|
|
6
|
-
import prompts from "prompts";
|
|
7
|
-
|
|
8
4
|
import { CLIENTS, getClient } from "../lib/clients.js";
|
|
9
5
|
import { installLumoTools, listInstalledBinaries } from "../lib/python.js";
|
|
10
6
|
import { findSkillSource, installSkill } from "../lib/skill.js";
|
|
11
7
|
import { registerMcp } from "../lib/mcp.js";
|
|
8
|
+
import { select } from "../lib/prompt.js";
|
|
9
|
+
import { bold, cyan, dim, green, yellow } from "../lib/style.js";
|
|
12
10
|
|
|
13
11
|
const __filename = fileURLToPath(import.meta.url);
|
|
14
12
|
const __dirname = path.dirname(__filename);
|
|
@@ -18,33 +16,29 @@ function devSource() {
|
|
|
18
16
|
return path.resolve(__dirname, "..", "..", "..", "tools");
|
|
19
17
|
}
|
|
20
18
|
|
|
21
|
-
async function
|
|
19
|
+
async function pickClients(supplied, allFlag) {
|
|
22
20
|
if (allFlag) return CLIENTS.filter((c) => c.id !== "generic");
|
|
23
21
|
if (supplied) return [getClient(supplied)];
|
|
24
22
|
|
|
25
|
-
const
|
|
26
|
-
type: "select",
|
|
27
|
-
name: "ai",
|
|
23
|
+
const ai = await select({
|
|
28
24
|
message: "Which AI client are you installing Lumo for?",
|
|
29
25
|
choices: CLIENTS.map((c) => ({ title: c.label, value: c.id })),
|
|
30
26
|
initial: 0,
|
|
31
27
|
});
|
|
32
|
-
if (!ai)
|
|
33
|
-
throw new Error("No client selected.");
|
|
34
|
-
}
|
|
28
|
+
if (!ai) throw new Error("No client selected.");
|
|
35
29
|
return [getClient(ai)];
|
|
36
30
|
}
|
|
37
31
|
|
|
38
32
|
export async function initCommand(opts) {
|
|
39
|
-
|
|
33
|
+
process.stdout.write("\n" + bold(cyan("• Lumo installer")) + "\n\n");
|
|
40
34
|
|
|
41
|
-
const targets = await
|
|
35
|
+
const targets = await pickClients(opts.ai, opts.all);
|
|
42
36
|
|
|
43
|
-
|
|
37
|
+
process.stdout.write(dim("→ installing Python tools (lumo-mobile) into ~/.lumo/venv ...\n"));
|
|
44
38
|
try {
|
|
45
39
|
if (opts.dev) {
|
|
46
40
|
const dev = devSource();
|
|
47
|
-
|
|
41
|
+
process.stdout.write(dim(` using --dev source ${dev}\n`));
|
|
48
42
|
await installLumoTools({ source: dev });
|
|
49
43
|
} else {
|
|
50
44
|
await installLumoTools();
|
|
@@ -61,39 +55,37 @@ export async function initCommand(opts) {
|
|
|
61
55
|
missing.map((b) => `${b.name} (expected at ${b.path})`).join("\n ")
|
|
62
56
|
);
|
|
63
57
|
}
|
|
64
|
-
|
|
65
|
-
bins.forEach((b) =>
|
|
58
|
+
process.stdout.write(green("✓ Python tools installed\n"));
|
|
59
|
+
bins.forEach((b) => process.stdout.write(dim(` ${b.name} ${b.path}\n`)));
|
|
66
60
|
|
|
67
61
|
const skillSource = findSkillSource();
|
|
68
62
|
|
|
69
63
|
for (const client of targets) {
|
|
70
|
-
|
|
64
|
+
process.stdout.write("\n" + bold(`→ ${client.label}`) + "\n");
|
|
71
65
|
if (!client.skillDir) {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
66
|
+
process.stdout.write(dim(" generic mode — nothing to copy automatically.\n"));
|
|
67
|
+
process.stdout.write(dim(" Skill bundle is at: ") + skillSource + "\n");
|
|
68
|
+
process.stdout.write(dim(" MCP server command: ") + listInstalledBinaries()[3].path + "\n");
|
|
75
69
|
continue;
|
|
76
70
|
}
|
|
77
71
|
|
|
78
72
|
installSkill(client.skillDir);
|
|
79
|
-
|
|
73
|
+
process.stdout.write(green(` ✓ skill copied to ${client.skillDir}\n`));
|
|
80
74
|
|
|
81
75
|
if (opts.mcp !== false && client.mcpConfigPath && client.mcpConfigKey) {
|
|
82
76
|
try {
|
|
83
77
|
const { configPath, command } = registerMcp(client.mcpConfigPath, client.mcpConfigKey);
|
|
84
|
-
|
|
85
|
-
|
|
78
|
+
process.stdout.write(green(` ✓ MCP registered in ${configPath}\n`));
|
|
79
|
+
process.stdout.write(dim(` command: ${command}\n`));
|
|
86
80
|
} catch (err) {
|
|
87
|
-
|
|
88
|
-
kleur.yellow(` ! MCP registration skipped: ${err.message}`)
|
|
89
|
-
);
|
|
81
|
+
process.stdout.write(yellow(` ! MCP registration skipped: ${err.message}\n`));
|
|
90
82
|
}
|
|
91
83
|
} else if (opts.mcp === false) {
|
|
92
|
-
|
|
84
|
+
process.stdout.write(dim(" MCP registration skipped (--no-mcp).\n"));
|
|
93
85
|
}
|
|
94
86
|
}
|
|
95
87
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
88
|
+
process.stdout.write("\n" + bold(green("✓ Lumo installation complete.")) + "\n\n");
|
|
89
|
+
process.stdout.write(dim("Next: open your AI client and ask it to use the Lumo skill.\n"));
|
|
90
|
+
process.stdout.write(dim("Verify any time with: lumo doctor\n\n"));
|
|
99
91
|
}
|
|
@@ -1,17 +1,14 @@
|
|
|
1
|
-
import kleur from "kleur";
|
|
2
|
-
import prompts from "prompts";
|
|
3
|
-
|
|
4
1
|
import { CLIENTS, getClient } from "../lib/clients.js";
|
|
5
2
|
import { unregisterMcp } from "../lib/mcp.js";
|
|
6
3
|
import { removeSkill } from "../lib/skill.js";
|
|
4
|
+
import { select } from "../lib/prompt.js";
|
|
5
|
+
import { bold, cyan, dim, green } from "../lib/style.js";
|
|
7
6
|
|
|
8
|
-
async function
|
|
7
|
+
async function pickClients(supplied, allFlag) {
|
|
9
8
|
if (allFlag) return CLIENTS.filter((c) => c.id !== "generic");
|
|
10
9
|
if (supplied) return [getClient(supplied)];
|
|
11
10
|
|
|
12
|
-
const
|
|
13
|
-
type: "select",
|
|
14
|
-
name: "ai",
|
|
11
|
+
const ai = await select({
|
|
15
12
|
message: "Remove Lumo from which client?",
|
|
16
13
|
choices: CLIENTS.filter((c) => c.skillDir).map((c) => ({ title: c.label, value: c.id })),
|
|
17
14
|
initial: 0,
|
|
@@ -21,31 +18,31 @@ async function pickClient(supplied, allFlag) {
|
|
|
21
18
|
}
|
|
22
19
|
|
|
23
20
|
export async function uninstallCommand(opts) {
|
|
24
|
-
|
|
21
|
+
process.stdout.write("\n" + bold(cyan("• Lumo uninstaller")) + "\n\n");
|
|
25
22
|
|
|
26
|
-
const targets = await
|
|
23
|
+
const targets = await pickClients(opts.ai, opts.all);
|
|
27
24
|
|
|
28
25
|
for (const client of targets) {
|
|
29
26
|
if (!client.skillDir) continue;
|
|
30
|
-
|
|
27
|
+
process.stdout.write("\n" + bold(`→ ${client.label}`) + "\n");
|
|
31
28
|
|
|
32
29
|
const removed = removeSkill(client.skillDir);
|
|
33
30
|
if (removed) {
|
|
34
|
-
|
|
31
|
+
process.stdout.write(green(` ✓ skill removed from ${client.skillDir}\n`));
|
|
35
32
|
} else {
|
|
36
|
-
|
|
33
|
+
process.stdout.write(dim(` skill not present (${client.skillDir})\n`));
|
|
37
34
|
}
|
|
38
35
|
|
|
39
36
|
if (client.mcpConfigPath && client.mcpConfigKey) {
|
|
40
37
|
const unreg = unregisterMcp(client.mcpConfigPath, client.mcpConfigKey);
|
|
41
38
|
if (unreg) {
|
|
42
|
-
|
|
39
|
+
process.stdout.write(green(` ✓ MCP entry removed from ${client.mcpConfigPath}\n`));
|
|
43
40
|
} else {
|
|
44
|
-
|
|
41
|
+
process.stdout.write(dim(` no MCP entry to remove\n`));
|
|
45
42
|
}
|
|
46
43
|
}
|
|
47
44
|
}
|
|
48
45
|
|
|
49
|
-
|
|
50
|
-
|
|
46
|
+
process.stdout.write("\n" + dim("Python tools (~/.lumo/venv) left intact.") + "\n");
|
|
47
|
+
process.stdout.write(dim("Remove them manually with: rm -rf ~/.lumo") + "\n\n");
|
|
51
48
|
}
|
package/src/index.js
CHANGED
|
@@ -3,57 +3,128 @@
|
|
|
3
3
|
* lumo — mobile UI/UX design intelligence installer.
|
|
4
4
|
*
|
|
5
5
|
* Subcommands:
|
|
6
|
-
* init [--ai <client>] [--all]
|
|
7
|
-
* doctor
|
|
8
|
-
* uninstall [--ai <client>]
|
|
6
|
+
* init [--ai <client>] [--all] [--no-mcp] [--dev]
|
|
7
|
+
* doctor
|
|
8
|
+
* uninstall [--ai <client>] [--all]
|
|
9
9
|
*
|
|
10
10
|
* Supported AI clients in v0.1: claude, cursor, codex, generic.
|
|
11
11
|
* Other clients can still consume Lumo via `npx skills add OneXeor-Dev/lumo`
|
|
12
12
|
* or by pointing their MCP config at `lumo-mcp`.
|
|
13
|
+
*
|
|
14
|
+
* Argument parsing uses node:util parseArgs (Node ≥ 18.3) — zero external
|
|
15
|
+
* dependencies, so Socket.dev sees nothing to flag at the dependency tree.
|
|
13
16
|
*/
|
|
14
17
|
|
|
15
|
-
import {
|
|
16
|
-
import kleur from "kleur";
|
|
18
|
+
import { parseArgs } from "node:util";
|
|
17
19
|
|
|
18
20
|
import { initCommand } from "./commands/init.js";
|
|
19
21
|
import { doctorCommand } from "./commands/doctor.js";
|
|
20
22
|
import { uninstallCommand } from "./commands/uninstall.js";
|
|
23
|
+
import { red } from "./lib/style.js";
|
|
24
|
+
|
|
25
|
+
const VERSION = "0.0.4";
|
|
26
|
+
|
|
27
|
+
const USAGE = `lumo — mobile UI/UX design intelligence installer (v${VERSION})
|
|
28
|
+
|
|
29
|
+
Usage:
|
|
30
|
+
lumo init [options] Install the Lumo skill into an AI coding assistant.
|
|
31
|
+
lumo doctor Verify Python tools, MCP server, and skill paths.
|
|
32
|
+
lumo uninstall [options] Remove the Lumo skill from an AI client.
|
|
33
|
+
lumo --help Show this message.
|
|
34
|
+
lumo --version Print the installer version.
|
|
35
|
+
|
|
36
|
+
init options:
|
|
37
|
+
-a, --ai <client> claude | cursor | codex | generic (asks if omitted)
|
|
38
|
+
--all Install into every supported client at once.
|
|
39
|
+
--no-mcp Skip registering the MCP server (skill-only).
|
|
40
|
+
--dev Install from the current git clone (contributors).
|
|
41
|
+
|
|
42
|
+
uninstall options:
|
|
43
|
+
-a, --ai <client> Which client to remove from (asks if omitted).
|
|
44
|
+
--all Remove from every supported client at once.
|
|
45
|
+
`;
|
|
46
|
+
|
|
47
|
+
function parseFlags(argv) {
|
|
48
|
+
// node:util parseArgs cannot do subcommands natively; we extract the
|
|
49
|
+
// first positional ourselves, then run parseArgs on the rest.
|
|
50
|
+
// parseArgs also doesn't recognise '--no-flag' as the negation of
|
|
51
|
+
// 'flag', so we translate manually before passing it on.
|
|
52
|
+
const [cmd, ...rawRest] = argv;
|
|
53
|
+
let mcpExplicitlyOff = false;
|
|
54
|
+
const rest = rawRest.filter((a) => {
|
|
55
|
+
if (a === "--no-mcp") {
|
|
56
|
+
mcpExplicitlyOff = true;
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
return true;
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const { values } = parseArgs({
|
|
63
|
+
args: rest,
|
|
64
|
+
allowPositionals: false,
|
|
65
|
+
strict: true,
|
|
66
|
+
options: {
|
|
67
|
+
ai: { type: "string", short: "a" },
|
|
68
|
+
all: { type: "boolean", default: false },
|
|
69
|
+
mcp: { type: "boolean", default: true },
|
|
70
|
+
dev: { type: "boolean", default: false },
|
|
71
|
+
help: { type: "boolean", short: "h", default: false },
|
|
72
|
+
version: { type: "boolean", short: "V", default: false },
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
if (mcpExplicitlyOff) values.mcp = false;
|
|
76
|
+
return { cmd, values };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function main() {
|
|
80
|
+
const argv = process.argv.slice(2);
|
|
81
|
+
|
|
82
|
+
// No subcommand → top-level help / version.
|
|
83
|
+
if (argv.length === 0 || argv[0] === "--help" || argv[0] === "-h") {
|
|
84
|
+
process.stdout.write(USAGE);
|
|
85
|
+
return 0;
|
|
86
|
+
}
|
|
87
|
+
if (argv[0] === "--version" || argv[0] === "-V") {
|
|
88
|
+
process.stdout.write(`${VERSION}\n`);
|
|
89
|
+
return 0;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
let parsed;
|
|
93
|
+
try {
|
|
94
|
+
parsed = parseFlags(argv);
|
|
95
|
+
} catch (err) {
|
|
96
|
+
process.stderr.write(red(`✗ ${err.message ?? err}\n`));
|
|
97
|
+
process.stderr.write("\nRun `lumo --help` for usage.\n");
|
|
98
|
+
return 2;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const { cmd, values } = parsed;
|
|
102
|
+
|
|
103
|
+
if (values.help) {
|
|
104
|
+
process.stdout.write(USAGE);
|
|
105
|
+
return 0;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
switch (cmd) {
|
|
109
|
+
case "init":
|
|
110
|
+
await initCommand(values);
|
|
111
|
+
return 0;
|
|
112
|
+
case "doctor":
|
|
113
|
+
await doctorCommand();
|
|
114
|
+
return 0;
|
|
115
|
+
case "uninstall":
|
|
116
|
+
await uninstallCommand(values);
|
|
117
|
+
return 0;
|
|
118
|
+
default:
|
|
119
|
+
process.stderr.write(red(`✗ unknown subcommand: ${cmd}\n`));
|
|
120
|
+
process.stderr.write("\nRun `lumo --help` for usage.\n");
|
|
121
|
+
return 2;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
21
124
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
"for Jetpack Compose, Android XML, SwiftUI, UIKit."
|
|
29
|
-
)
|
|
30
|
-
.version("0.0.3");
|
|
31
|
-
|
|
32
|
-
program
|
|
33
|
-
.command("init")
|
|
34
|
-
.description("Install the Lumo skill into an AI coding assistant.")
|
|
35
|
-
.option(
|
|
36
|
-
"-a, --ai <client>",
|
|
37
|
-
"Target client: claude | cursor | codex | generic (asks interactively if omitted)"
|
|
38
|
-
)
|
|
39
|
-
.option("--all", "Install into every supported client at once.")
|
|
40
|
-
.option("--no-mcp", "Skip registering the MCP server (skill-only install).")
|
|
41
|
-
.option("--dev", "Install from the current git clone instead of pip (for contributors).")
|
|
42
|
-
.action(initCommand);
|
|
43
|
-
|
|
44
|
-
program
|
|
45
|
-
.command("doctor")
|
|
46
|
-
.description("Verify Python tools, MCP server, and skill installation paths.")
|
|
47
|
-
.action(doctorCommand);
|
|
48
|
-
|
|
49
|
-
program
|
|
50
|
-
.command("uninstall")
|
|
51
|
-
.description("Remove the Lumo skill from an AI client (Python tools left intact).")
|
|
52
|
-
.option("-a, --ai <client>", "Which client to remove from (asks interactively if omitted)")
|
|
53
|
-
.option("--all", "Remove from every supported client at once.")
|
|
54
|
-
.action(uninstallCommand);
|
|
55
|
-
|
|
56
|
-
program.parseAsync(process.argv).catch((err) => {
|
|
57
|
-
console.error(kleur.red(`\n✗ ${err.message ?? err}`));
|
|
58
|
-
process.exit(1);
|
|
59
|
-
});
|
|
125
|
+
main()
|
|
126
|
+
.then((code) => process.exit(code ?? 0))
|
|
127
|
+
.catch((err) => {
|
|
128
|
+
process.stderr.write(red(`\n✗ ${err.message ?? err}\n`));
|
|
129
|
+
process.exit(1);
|
|
130
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tiny terminal `select` prompt. Self-contained replacement for `prompts`.
|
|
3
|
+
*
|
|
4
|
+
* Why: Socket.dev flagged `prompts` because it depends on `sisteransi`
|
|
5
|
+
* which hasn't been updated in 5+ years (Maintenance alert). Our usage
|
|
6
|
+
* is one select-from-list prompt; rolling our own keeps the package
|
|
7
|
+
* dependency tree empty and removes the unmaintained-transitive alert.
|
|
8
|
+
*
|
|
9
|
+
* Renders a numbered list, waits for one keystroke or a typed number
|
|
10
|
+
* followed by Enter. Plays well with raw-mode TTY. Falls back to
|
|
11
|
+
* answer 0 (first option) when stdin is not a TTY (CI, piping).
|
|
12
|
+
*
|
|
13
|
+
* Returns the value of the chosen option, or undefined if the user
|
|
14
|
+
* aborted with Ctrl-C / Ctrl-D / Esc.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import readline from "node:readline";
|
|
18
|
+
|
|
19
|
+
import { bold, cyan, dim } from "./style.js";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @typedef {object} Choice
|
|
23
|
+
* @property {string} title
|
|
24
|
+
* @property {any} value
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @param {object} opts
|
|
29
|
+
* @param {string} opts.message
|
|
30
|
+
* @param {Choice[]} opts.choices
|
|
31
|
+
* @param {number} [opts.initial]
|
|
32
|
+
* @returns {Promise<any | undefined>}
|
|
33
|
+
*/
|
|
34
|
+
export function select({ message, choices, initial = 0 }) {
|
|
35
|
+
return new Promise((resolve) => {
|
|
36
|
+
const stdout = process.stdout;
|
|
37
|
+
const stdin = process.stdin;
|
|
38
|
+
|
|
39
|
+
// Non-interactive: pick the initial choice and move on.
|
|
40
|
+
if (!stdin.isTTY) {
|
|
41
|
+
stdout.write(`${cyan("?")} ${message} ${dim(`(non-interactive, picking ${choices[initial].title})`)}\n`);
|
|
42
|
+
resolve(choices[initial].value);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
stdout.write(`${cyan("?")} ${bold(message)}\n`);
|
|
47
|
+
for (let i = 0; i < choices.length; i++) {
|
|
48
|
+
const marker = i === initial ? cyan("›") : " ";
|
|
49
|
+
stdout.write(` ${marker} ${i + 1}. ${choices[i].title}\n`);
|
|
50
|
+
}
|
|
51
|
+
stdout.write(dim(` (use 1-${choices.length} then Enter, or Ctrl-C to abort)\n`));
|
|
52
|
+
|
|
53
|
+
const rl = readline.createInterface({
|
|
54
|
+
input: stdin,
|
|
55
|
+
output: stdout,
|
|
56
|
+
terminal: true,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
rl.question("> ", (raw) => {
|
|
60
|
+
rl.close();
|
|
61
|
+
const trimmed = raw.trim();
|
|
62
|
+
if (trimmed === "") {
|
|
63
|
+
resolve(choices[initial].value);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const idx = Number.parseInt(trimmed, 10) - 1;
|
|
67
|
+
if (!Number.isInteger(idx) || idx < 0 || idx >= choices.length) {
|
|
68
|
+
resolve(undefined);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
resolve(choices[idx].value);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
rl.on("SIGINT", () => {
|
|
75
|
+
rl.close();
|
|
76
|
+
resolve(undefined);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
}
|
package/src/lib/style.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tiny ANSI styling helper. Self-contained replacement for `kleur`.
|
|
3
|
+
*
|
|
4
|
+
* Why: Socket.dev flags packages whose dependency tree contains
|
|
5
|
+
* environment-variable access (kleur reads NO_COLOR / FORCE_COLOR
|
|
6
|
+
* which is legitimate but raises a supply-chain risk signal). We use
|
|
7
|
+
* exactly four colours in the installer; rolling our own keeps
|
|
8
|
+
* `dependencies: {}` empty.
|
|
9
|
+
*
|
|
10
|
+
* Colours are emitted unconditionally. If you need NO_COLOR support
|
|
11
|
+
* later, gate the prefix strings on `process.env.NO_COLOR` here in
|
|
12
|
+
* one place.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const ESC = "\x1b";
|
|
16
|
+
|
|
17
|
+
function wrap(open, close) {
|
|
18
|
+
return (text) => `${ESC}[${open}m${text}${ESC}[${close}m`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const cyan = wrap(36, 39);
|
|
22
|
+
export const green = wrap(32, 39);
|
|
23
|
+
export const yellow = wrap(33, 39);
|
|
24
|
+
export const red = wrap(31, 39);
|
|
25
|
+
export const dim = wrap(2, 22);
|
|
26
|
+
export const bold = wrap(1, 22);
|
|
27
|
+
|
|
28
|
+
/** Compose: `bold(cyan("text"))` works because every fn is `text -> text`. */
|