@onexeor/lumo 0.0.2 → 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/README.md CHANGED
@@ -42,6 +42,24 @@ Under the hood the installer:
42
42
  The installer never modifies anything outside `~/.lumo`, the chosen
43
43
  client's skill directory, and the chosen client's MCP config file.
44
44
 
45
+ ## Publishing (maintainers only)
46
+
47
+ The installer ships with no npm lifecycle scripts on purpose — Socket.dev
48
+ and similar supply-chain scanners penalise packages that auto-execute
49
+ code on install or pack, even when that code is benign. To publish a
50
+ new version:
51
+
52
+ ```bash
53
+ cd installer
54
+ bash scripts/release.sh --dry-run # bundles /skill into installer/skill, npm pack --dry-run
55
+ bash scripts/release.sh # same, but actually publishes
56
+ ```
57
+
58
+ `release.sh` is the only sanctioned way to build the tarball. Running
59
+ `npm pack` or `npm publish` directly will produce a broken tarball
60
+ without the bundled `SKILL.md`, so `lumo init` would fail
61
+ post-install. The script makes the bundling explicit and Socket-clean.
62
+
45
63
  ## License
46
64
 
47
65
  MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onexeor/lumo",
3
- "version": "0.0.2",
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": {
@@ -17,7 +17,7 @@
17
17
  },
18
18
  "scripts": {
19
19
  "start": "node src/index.js",
20
- "prepack": "node scripts/bundle-skill.js"
20
+ "bundle-skill": "node scripts/bundle-skill.js"
21
21
  },
22
22
  "keywords": [
23
23
  "mobile",
@@ -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/skill/SKILL.md CHANGED
@@ -43,7 +43,15 @@ mobile UI concern, stay out.
43
43
 
44
44
  Lumo ships with Python tools. Each tool has a single CLI entry-point with
45
45
  deterministic output. Invoke via Bash. Tools live in `tools/lumo/<area>/cli.py`
46
- and are exposed as console scripts after `pip install -e tools/`.
46
+ and are exposed as console scripts after `pip install -e tools/` or
47
+ `pipx install lumo-mobile`.
48
+
49
+ When running inside an MCP-aware client (Cursor, Continue, Aider, Goose,
50
+ Zed, Codex, or Claude Code with MCP enabled), the same four tools are
51
+ also exposed as MCP functions: `lumo_wcag_check`, `lumo_wcag_fix`,
52
+ `lumo_theory_check`, `lumo_parity_diff`. Prefer the MCP function over
53
+ spawning a Bash subprocess when available — the structured response is
54
+ already JSON and the user does not see noisy command output.
47
55
 
48
56
  ### `lumo-wcag` — WCAG contrast validator + OKLCH auto-correct
49
57
 
@@ -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 ? kleur.green("✓") : kleur.red("✗");
10
- const text = ok ? kleur.white(label) : kleur.yellow(label);
11
- console.log(` ${icon} ${text}${detail ? kleur.dim(" " + detail) : ""}`);
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
- console.log(kleur.bold().cyan("\n• lumo doctor\n"));
15
+ process.stdout.write("\n" + bold(cyan("• lumo doctor")) + "\n\n");
16
16
 
17
- console.log(kleur.bold("Python tools"));
18
- row(fs.existsSync(LUMO_HOME), `Lumo home`, LUMO_HOME);
19
- row(fs.existsSync(venvBinary("python")), `venv Python`, 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
- console.log("");
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
- console.log("");
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
  }
@@ -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 pickClient(supplied, allFlag) {
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 { ai } = await prompts({
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
- console.log(kleur.bold().cyan("\n• Lumo installer\n"));
33
+ process.stdout.write("\n" + bold(cyan("• Lumo installer")) + "\n\n");
40
34
 
41
- const targets = await pickClient(opts.ai, opts.all);
35
+ const targets = await pickClients(opts.ai, opts.all);
42
36
 
43
- console.log(kleur.dim("→ installing Python tools (lumo-mobile) into ~/.lumo/venv ..."));
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
- console.log(kleur.dim(` using --dev source ${dev}`));
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
- console.log(kleur.green(`✓ Python tools installed`));
65
- bins.forEach((b) => console.log(kleur.dim(` ${b.name} ${b.path}`)));
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
- console.log(kleur.bold(`\n ${client.label}`));
64
+ process.stdout.write("\n" + bold(`→ ${client.label}`) + "\n");
71
65
  if (!client.skillDir) {
72
- console.log(kleur.dim(" generic mode — nothing to copy automatically."));
73
- console.log(kleur.dim(" Skill bundle is at: ") + skillSource);
74
- console.log(kleur.dim(" MCP server command: ") + listInstalledBinaries()[3].path);
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
- console.log(kleur.green(` ✓ skill copied to ${client.skillDir}`));
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
- console.log(kleur.green(` ✓ MCP registered in ${configPath}`));
85
- console.log(kleur.dim(` command: ${command}`));
78
+ process.stdout.write(green(` ✓ MCP registered in ${configPath}\n`));
79
+ process.stdout.write(dim(` command: ${command}\n`));
86
80
  } catch (err) {
87
- console.log(
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
- console.log(kleur.dim(" MCP registration skipped (--no-mcp)."));
84
+ process.stdout.write(dim(" MCP registration skipped (--no-mcp).\n"));
93
85
  }
94
86
  }
95
87
 
96
- console.log(kleur.bold().green("\n✓ Lumo installation complete.\n"));
97
- console.log(kleur.dim("Next: open your AI client and ask it to use the Lumo skill."));
98
- console.log(kleur.dim("Verify any time with: lumo doctor\n"));
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 pickClient(supplied, allFlag) {
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 { ai } = await prompts({
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
- console.log(kleur.bold().cyan("\n• Lumo uninstaller\n"));
21
+ process.stdout.write("\n" + bold(cyan("• Lumo uninstaller")) + "\n\n");
25
22
 
26
- const targets = await pickClient(opts.ai, opts.all);
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
- console.log(kleur.bold(`\n ${client.label}`));
27
+ process.stdout.write("\n" + bold(`→ ${client.label}`) + "\n");
31
28
 
32
29
  const removed = removeSkill(client.skillDir);
33
30
  if (removed) {
34
- console.log(kleur.green(` ✓ skill removed from ${client.skillDir}`));
31
+ process.stdout.write(green(` ✓ skill removed from ${client.skillDir}\n`));
35
32
  } else {
36
- console.log(kleur.dim(` skill not present (${client.skillDir})`));
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
- console.log(kleur.green(` ✓ MCP entry removed from ${client.mcpConfigPath}`));
39
+ process.stdout.write(green(` ✓ MCP entry removed from ${client.mcpConfigPath}\n`));
43
40
  } else {
44
- console.log(kleur.dim(` no MCP entry to remove`));
41
+ process.stdout.write(dim(` no MCP entry to remove\n`));
45
42
  }
46
43
  }
47
44
  }
48
45
 
49
- console.log(kleur.dim("\nPython tools (~/.lumo/venv) left intact."));
50
- console.log(kleur.dim("Remove them manually with: rm -rf ~/.lumo\n"));
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] Install the Lumo skill (+ optional MCP) into an AI client.
7
- * doctor Verify Python tools + skill installation.
8
- * uninstall [--ai <client>] Remove the Lumo skill (Python tools left intact).
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 { Command } from "commander";
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
- const program = new Command();
23
-
24
- program
25
- .name("lumo")
26
- .description(
27
- "Mobile UI/UX design intelligence — WCAG / parity / cognitive-science checks " +
28
- "for Jetpack Compose, Android XML, SwiftUI, UIKit."
29
- )
30
- .version("0.0.2");
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
+ }
@@ -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`. */