chore: updated CLI 3e2df695
Steve · 2025-09-21 17:12 4 file(s) · +468 −52
README.md +2 −2
12 12
13 13
The goal of Norns is to provide the Ethereum ecosystem a set of simple yet powerful web components for building decentralized applications. The advantage we have today is that we've experienced good DX from modern frameworks, so we have the ability to build components that feel familiar to devs building UIs for smart contracts. We will start small but slowly grow the offering as we get a better feel for what devs need; check out the [Roadmap](#roadmap) for more information.
14 14
15 -
## Developer Setup
15 +
## Local Development Setup
16 16
17 17
1. Clone and install dependencies with [Bun](https://bun.sh)
18 18
41 41
**CLI**
42 42
43 43
- [x] Implement `norns.json` initialization
44 -
- [ ] Improve styles and UX of commands and help menus
44 +
- [x] Improve styles and UX of commands and help menus
45 45
46 46
**Components**
47 47
src/index.ts +131 −50
9 9
10 10
import { fileURLToPath } from "url";
11 11
import { dirname } from "path";
12 +
import * as colors from "./utils/colors.js";
13 +
import yoctoSpinner from "./utils/spinner.js";
12 14
13 15
const __filename = fileURLToPath(import.meta.url);
14 16
32 34
		const configContent = await readFile(CONFIG_FILE, "utf8");
33 35
		return JSON.parse(configContent);
34 36
	} catch (error) {
35 -
		console.error(`✗ Failed to load ${CONFIG_FILE}:`, error);
37 +
		console.error(colors.red(`✗ Failed to load ${CONFIG_FILE}:`), error);
36 38
		return null;
37 39
	}
38 40
}
39 41
40 42
async function saveConfig(config: NornsConfig): Promise<void> {
43 +
	const spinner = yoctoSpinner({ text: `Saving ${CONFIG_FILE}` }).start();
41 44
	try {
42 45
		await writeFile(CONFIG_FILE, JSON.stringify(config, null, 2), "utf8");
46 +
		spinner.success(`Saved ${CONFIG_FILE}`);
43 47
	} catch (error) {
44 -
		console.error(`✗ Failed to save ${CONFIG_FILE}:`, error);
48 +
		spinner.error(`Failed to save ${CONFIG_FILE}`);
49 +
		console.error(error);
45 50
		throw error;
46 51
	}
47 52
}
68 73
}
69 74
70 75
async function init() {
71 -
	console.log("⚡ Initializing norns project...");
76 +
	console.log(colors.yellow("∅ Initializing norns project..."));
72 77
73 78
	// Check if components.json already exists
74 79
	if (existsSync(CONFIG_FILE)) {
75 -
		console.log(`▸ ${CONFIG_FILE} already exists`);
80 +
		console.log(colors.blue(`▸ ${CONFIG_FILE} already exists`));
76 81
		const overwrite = await promptUser(
77 82
			"Would you like to overwrite it? (y/N)",
78 83
			"n",
79 84
		);
80 85
		if (overwrite.toLowerCase() !== "y" && overwrite.toLowerCase() !== "yes") {
81 -
			console.log("✗ Initialization cancelled");
86 +
			console.log(colors.red("✗ Initialization cancelled"));
82 87
			return;
83 88
		}
84 89
	}
85 90
86 -
	console.log("\n▸ Setting up your components configuration...\n");
91 +
	console.log(colors.blue("\n▸ Setting up your components configuration...\n"));
87 92
88 93
	// Get component directory path
89 94
	const componentsPath = await promptUser(
98 103
99 104
	// Create components directory if it doesn't exist
100 105
	if (!existsSync(componentsPath)) {
106 +
		const dirSpinner = yoctoSpinner({
107 +
			text: `Creating ${componentsPath} directory`,
108 +
		}).start();
101 109
		await mkdir(componentsPath, { recursive: true });
102 -
		console.log(`✓ Created ${componentsPath} directory`);
110 +
		dirSpinner.success(`Created ${componentsPath} directory`);
103 111
	}
104 112
105 113
	// Save the configuration
106 114
	await saveConfig(config);
107 -
	console.log(`✓ Created ${CONFIG_FILE}`);
108 115
109 116
	console.log(
110 -
		"\n✓ norns project initialized! You can now add components with:",
117 +
		colors.green(
118 +
			"\n✓ norns project initialized! You can now add components with:",
119 +
		),
111 120
	);
112 -
	console.log("  npx norns@latest add <component-name>");
113 -
	console.log(`\n▸ Components will be installed to: ${componentsPath}`);
121 +
	console.log(colors.cyan("  npx norns@latest add <component-name>"));
122 +
	console.log(
123 +
		colors.blue(`\n▸ Components will be installed to: ${componentsPath}`),
124 +
	);
114 125
}
115 126
116 127
async function addComponent(componentName: string | undefined) {
117 128
	if (!componentName) {
118 -
		console.error("✗ Please specify a component name");
119 -
		console.log("Usage: npx norns@latest add <component-name>");
129 +
		console.error(colors.red("✗ Please specify a component name"));
130 +
		console.log(colors.cyan("Usage: npx norns@latest add <component-name>"));
120 131
		process.exit(1);
121 132
	}
122 133
123 -
	console.log(`▸ Adding component: ${componentName}`);
134 +
	console.log(colors.blue(`▸ Adding component: ${componentName}`));
124 135
125 136
	// Load configuration
126 137
	let config = await loadConfig();
127 138
128 139
	// If no config exists, ask user to run init first or use defaults
129 140
	if (!config) {
130 -
		console.log("▸ No norns.json found.");
141 +
		console.log(colors.blue("▸ No norns.json found."));
131 142
		const shouldInit = await promptUser(
132 143
			"Would you like to run 'norns init' first? (Y/n)",
133 144
			"y",
141 152
			await init();
142 153
			config = await loadConfig();
143 154
		} else {
144 -
			console.log("▸ Using default configuration...");
155 +
			console.log(colors.blue("▸ Using default configuration..."));
145 156
			config = DEFAULT_CONFIG;
146 157
		}
147 158
	}
148 159
149 160
	if (!config) {
150 -
		console.error("✗ Failed to initialize configuration");
161 +
		console.error(colors.red("✗ Failed to initialize configuration"));
151 162
		process.exit(1);
152 163
	}
153 164
155 166
156 167
	// Create components directory if it doesn't exist
157 168
	if (!existsSync(componentsDir)) {
158 -
		console.log(
159 -
			`▸ Components directory doesn't exist. Creating ${componentsDir}...`,
160 -
		);
169 +
		const dirSpinner = yoctoSpinner({
170 +
			text: `Creating ${componentsDir} directory`,
171 +
		}).start();
161 172
		await mkdir(componentsDir, { recursive: true });
173 +
		dirSpinner.success(`Created ${componentsDir} directory`);
162 174
	}
163 175
164 176
	try {
165 177
		const sourceComponentPath = join(COMPONENTS_DIR, `${componentName}.js`);
166 178
167 179
		if (!existsSync(sourceComponentPath)) {
168 -
			console.error(`✗ Component '${componentName}' not found`);
169 -
			console.log("Available components:");
170 -
			console.log("  - connect-wallet");
171 -
			console.log("  - contract-call");
180 +
			console.error(colors.red(`✗ Component '${componentName}' not found`));
181 +
			console.log(colors.blue("Available components:"));
182 +
			console.log(colors.cyan("  - connect-wallet"));
183 +
			console.log(colors.cyan("  - contract-call"));
172 184
			process.exit(1);
173 185
		}
186 +
187 +
		const installSpinner = yoctoSpinner({
188 +
			text: `Installing ${componentName} component`,
189 +
		}).start();
174 190
175 191
		const componentCode = await readFile(sourceComponentPath, "utf8");
176 192
		const componentPath = join(componentsDir, `${componentName}.js`);
177 193
178 194
		await writeFile(componentPath, componentCode, "utf8");
179 195
180 -
		console.log(`✓ Added ${componentName} to ${componentPath}`);
181 -
		console.log(`▸ You can now use it in your HTML:`);
182 -
		console.log(`   <script src="components/${componentName}.js"></script>`);
183 -
		console.log(`   <${componentName}></${componentName}>`);
196 +
		installSpinner.success(`Added ${componentName} to ${componentPath}`);
197 +
		console.log(colors.blue(`▸ You can now use it in your HTML:`));
198 +
		console.log(
199 +
			colors.cyan(`   <script src="components/${componentName}.js"></script>`),
200 +
		);
201 +
		console.log(colors.cyan(`   <${componentName}></${componentName}>`));
184 202
	} catch (error) {
185 -
		console.error(`✗ Failed to add component: ${error}`);
203 +
		console.error(colors.red(`✗ Failed to add component: ${error}`));
186 204
		process.exit(1);
187 205
	}
188 206
}
189 207
190 208
function showHelp() {
191 -
	console.log(`
192 -
⚡ norns - Web Component Library CLI
209 +
	console.log(
210 +
		colors.yellow(`
193 211
194 -
Usage:
195 -
  npx norns@latest init                    Initialize a new norns project with norns.json
196 -
  npx norns@latest add <component-name>    Add a component to your project
197 -
  npx norns@latest --help                  Show this help message
212 +
                                 @
213 +
                                @@@@
214 +
                                  @@@@    @@@@@@@@@
215 +
                                    @@@@  @@@   @@@@@@
216 +
                                      @@@@         @@@@
217 +
                                    @@  @@@@        @@@@
218 +
                                  @@@@    @@@@       @@@
219 +
                   @@@@@@@      @@@@        @@@@    @@@
220 +
                 @@@@@@@@@@@  @@@@            @@@  @@@@
221 +
               @@@@         @@@@                 @@@@
222 +
              @@@         @@@@  @              @@@@  @
223 +
              @@@       @@@@   @@@@          @@@@   @@@@          @@@
224 +
              @@@     @@@@       @@@@      @@@@       @@@@      @@@@
225 +
               @@@  @@@@           @@@@  @@@@           @@@@  @@@@
226 +
                @@@@                 @@@@                 @@@@
227 +
                  @@@@                 @@@@                 @@@@
228 +
              @@@@  @@@@           @@@@  @@@@           @@@@  @@@
229 +
            @@@@      @@@@       @@@@      @@@@       @@@@     @@@
230 +
           @@@          @@@@   @@@@          @@@@   @@@@       @@@
231 +
                          @  @@@@              @  @@@@         @@@
232 +
                           @@@@                 @@@@         @@@@
233 +
                         @@@@  @@@            @@@@  @@@@@@@@@@@
234 +
                         @@@    @@@@        @@@@      @@@@@@
235 +
                        @@@       @@@@    @@@@
236 +
                        @@@@        @@@@  @@
237 +
                         @@@@         @@@@
238 +
                           @@@@@   @@@  @@@@
239 +
                             @@@@@@@@@    @@@@
240 +
                                            @@@@
241 +
                                              @
242 +
`),
243 +
	);
244 +
	console.log(
245 +
		colors.yellow(`\n∅ norns - web components for decentralized applications`),
246 +
		colors.cyan("\nhttps://github.com/stevedylandev/norns\n"),
247 +
	);
248 +
	console.log(colors.bold("Usage:"));
249 +
	console.log(
250 +
		colors.cyan(
251 +
			"  npx norns@latest init                    Initialize a new norns project with norns.json",
252 +
		),
253 +
	);
254 +
	console.log(
255 +
		colors.cyan(
256 +
			"  npx norns@latest add <component-name>    Add a component to your project",
257 +
		),
258 +
	);
259 +
	console.log(
260 +
		colors.cyan(
261 +
			"  npx norns@latest --help                  Show this help message\n",
262 +
		),
263 +
	);
198 264
199 -
Examples:
200 -
  npx norns@latest init
201 -
  npx norns@latest add connect-wallet
265 +
	console.log(colors.bold("Examples:"));
266 +
	console.log(colors.green("  npx norns@latest init"));
267 +
	console.log(colors.green("  npx norns@latest add connect-wallet\n"));
202 268
203 -
The init command will:
204 -
  - Create a norns.json configuration file
205 -
  - Set up your preferred component installation directory
206 -
  - Create necessary directories
269 +
	console.log(colors.bold("The init command will:"));
270 +
	console.log(colors.blue("  - Create a norns.json configuration file"));
271 +
	console.log(
272 +
		colors.blue("  - Set up your preferred component installation directory"),
273 +
	);
274 +
	console.log(colors.blue("  - Create necessary directories\n"));
207 275
208 -
Available Components:
209 -
  - connect-wallet    A Web3 wallet connection component
210 -
  - contract-call     A Web3 contract interaction component
276 +
	console.log(colors.bold("Available Components:"));
277 +
	console.log(
278 +
		colors.cyan("  - connect-wallet    A Web3 wallet connection component"),
279 +
	);
280 +
	console.log(
281 +
		colors.cyan(
282 +
			"  - contract-call     A Web3 contract interaction component\n",
283 +
		),
284 +
	);
211 285
212 -
Configuration:
213 -
  The norns.json file controls where components are installed.
214 -
  You can customize the installation directory during init or edit the file directly.
215 -
`);
286 +
	console.log(colors.bold("Configuration:"));
287 +
	console.log(
288 +
		colors.blue(
289 +
			"  The norns.json file controls where components are installed.",
290 +
		),
291 +
	);
292 +
	console.log(
293 +
		colors.blue(
294 +
			"  You can customize the installation directory during init or edit the file directly.",
295 +
		),
296 +
	);
216 297
}
217 298
218 299
// Parse command line arguments using Node's parseArgs
247 328
			if (!command) {
248 329
				showHelp();
249 330
			} else {
250 -
				console.error(`✗ Unknown command: ${command}`);
331 +
				console.error(colors.red(`✗ Unknown command: ${command}`));
251 332
				showHelp();
252 333
				process.exit(1);
253 334
			}
src/utils/colors.js (added) +94 −0
1 +
import tty from "node:tty";
2 +
3 +
// eslint-disable-next-line no-warning-comments
4 +
// TODO: Use a better method when it's added to Node.js (https://github.com/nodejs/node/pull/40240)
5 +
// Lots of optionals here to support Deno.
6 +
const hasColors = tty?.WriteStream?.prototype?.hasColors?.() ?? false;
7 +
8 +
const format = (open, close) => {
9 +
	if (!hasColors) {
10 +
		return (input) => input;
11 +
	}
12 +
13 +
	const openCode = `\u001B[${open}m`;
14 +
	const closeCode = `\u001B[${close}m`;
15 +
16 +
	return (input) => {
17 +
		const string = input + ""; // eslint-disable-line no-implicit-coercion -- This is faster.
18 +
		let index = string.indexOf(closeCode);
19 +
20 +
		if (index === -1) {
21 +
			// Note: Intentionally not using string interpolation for performance reasons.
22 +
			return openCode + string + closeCode;
23 +
		}
24 +
25 +
		// Handle nested colors.
26 +
27 +
		// We could have done this, but it's too slow (as of Node.js 22).
28 +
		// return openCode + string.replaceAll(closeCode, (close === 22 ? closeCode : '') + openCode) + closeCode;
29 +
30 +
		let result = openCode;
31 +
		let lastIndex = 0;
32 +
33 +
		// SGR 22 resets both bold (1) and dim (2). When we encounter a nested
34 +
		// close for styles that use 22, we need to re-open the outer style.
35 +
		const reopenOnNestedClose = close === 22;
36 +
		const replaceCode = (reopenOnNestedClose ? closeCode : "") + openCode;
37 +
38 +
		while (index !== -1) {
39 +
			result += string.slice(lastIndex, index) + replaceCode;
40 +
			lastIndex = index + closeCode.length;
41 +
			index = string.indexOf(closeCode, lastIndex);
42 +
		}
43 +
44 +
		result += string.slice(lastIndex) + closeCode;
45 +
46 +
		return result;
47 +
	};
48 +
};
49 +
50 +
export const reset = format(0, 0);
51 +
export const bold = format(1, 22);
52 +
export const dim = format(2, 22);
53 +
export const italic = format(3, 23);
54 +
export const underline = format(4, 24);
55 +
export const overline = format(53, 55);
56 +
export const inverse = format(7, 27);
57 +
export const hidden = format(8, 28);
58 +
export const strikethrough = format(9, 29);
59 +
60 +
export const black = format(30, 39);
61 +
export const red = format(31, 39);
62 +
export const green = format(32, 39);
63 +
export const yellow = format(33, 39);
64 +
export const blue = format(34, 39);
65 +
export const magenta = format(35, 39);
66 +
export const cyan = format(36, 39);
67 +
export const white = format(37, 39);
68 +
export const gray = format(90, 39);
69 +
70 +
export const bgBlack = format(40, 49);
71 +
export const bgRed = format(41, 49);
72 +
export const bgGreen = format(42, 49);
73 +
export const bgYellow = format(43, 49);
74 +
export const bgBlue = format(44, 49);
75 +
export const bgMagenta = format(45, 49);
76 +
export const bgCyan = format(46, 49);
77 +
export const bgWhite = format(47, 49);
78 +
export const bgGray = format(100, 49);
79 +
80 +
export const redBright = format(91, 39);
81 +
export const greenBright = format(92, 39);
82 +
export const yellowBright = format(93, 39);
83 +
export const blueBright = format(94, 39);
84 +
export const magentaBright = format(95, 39);
85 +
export const cyanBright = format(96, 39);
86 +
export const whiteBright = format(97, 39);
87 +
88 +
export const bgRedBright = format(101, 49);
89 +
export const bgGreenBright = format(102, 49);
90 +
export const bgYellowBright = format(103, 49);
91 +
export const bgBlueBright = format(104, 49);
92 +
export const bgMagentaBright = format(105, 49);
93 +
export const bgCyanBright = format(106, 49);
94 +
export const bgWhiteBright = format(107, 49);
src/utils/spinner.js (added) +241 −0
1 +
// https://github.com/sindresorhus/yocto-spinner/blob/main/index.js
2 +
import process from "node:process";
3 +
import { stripVTControlCharacters } from "node:util";
4 +
import * as yoctocolors from "./colors";
5 +
6 +
const isUnicodeSupported =
7 +
	process.platform !== "win32" ||
8 +
	Boolean(process.env.WT_SESSION) || // Windows Terminal
9 +
	process.env.TERM_PROGRAM === "vscode";
10 +
11 +
const isInteractive = (stream) =>
12 +
	Boolean(
13 +
		stream.isTTY && process.env.TERM !== "dumb" && !("CI" in process.env),
14 +
	);
15 +
16 +
const infoSymbol = yoctocolors.blue(isUnicodeSupported ? "ℹ" : "i");
17 +
const successSymbol = yoctocolors.green(isUnicodeSupported ? "✔" : "√");
18 +
const warningSymbol = yoctocolors.yellow(isUnicodeSupported ? "⚠" : "‼");
19 +
const errorSymbol = yoctocolors.red(isUnicodeSupported ? "✖" : "×");
20 +
21 +
const defaultSpinner = {
22 +
	frames: isUnicodeSupported
23 +
		? ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
24 +
		: ["-", "\\", "|", "/"],
25 +
	interval: 80,
26 +
};
27 +
28 +
class YoctoSpinner {
29 +
	#frames;
30 +
	#interval;
31 +
	#currentFrame = -1;
32 +
	#timer;
33 +
	#text;
34 +
	#stream;
35 +
	#color;
36 +
	#lines = 0;
37 +
	#exitHandlerBound;
38 +
	#isInteractive;
39 +
	#lastSpinnerFrameTime = 0;
40 +
	#isSpinning = false;
41 +
42 +
	constructor(options = {}) {
43 +
		const spinner = options.spinner ?? defaultSpinner;
44 +
		this.#frames = spinner.frames;
45 +
		this.#interval = spinner.interval;
46 +
		this.#text = options.text ?? "";
47 +
		this.#stream = options.stream ?? process.stderr;
48 +
		this.#color = options.color ?? "cyan";
49 +
		this.#isInteractive = isInteractive(this.#stream);
50 +
		this.#exitHandlerBound = this.#exitHandler.bind(this);
51 +
	}
52 +
53 +
	start(text) {
54 +
		if (text) {
55 +
			this.#text = text;
56 +
		}
57 +
58 +
		if (this.isSpinning) {
59 +
			return this;
60 +
		}
61 +
62 +
		this.#isSpinning = true;
63 +
		this.#hideCursor();
64 +
		this.#render();
65 +
		this.#subscribeToProcessEvents();
66 +
67 +
		// Only start the timer in interactive mode
68 +
		if (this.#isInteractive) {
69 +
			this.#timer = setInterval(() => {
70 +
				this.#render();
71 +
			}, this.#interval);
72 +
		}
73 +
74 +
		return this;
75 +
	}
76 +
77 +
	stop(finalText) {
78 +
		if (!this.isSpinning) {
79 +
			return this;
80 +
		}
81 +
82 +
		this.#isSpinning = false;
83 +
		if (this.#timer) {
84 +
			clearInterval(this.#timer);
85 +
			this.#timer = undefined;
86 +
		}
87 +
88 +
		this.#showCursor();
89 +
		this.clear();
90 +
		this.#unsubscribeFromProcessEvents();
91 +
92 +
		if (finalText) {
93 +
			this.#stream.write(`${finalText}\n`);
94 +
		}
95 +
96 +
		return this;
97 +
	}
98 +
99 +
	#symbolStop(symbol, text) {
100 +
		return this.stop(`${symbol} ${text ?? this.#text}`);
101 +
	}
102 +
103 +
	success(text) {
104 +
		return this.#symbolStop(successSymbol, text);
105 +
	}
106 +
107 +
	error(text) {
108 +
		return this.#symbolStop(errorSymbol, text);
109 +
	}
110 +
111 +
	warning(text) {
112 +
		return this.#symbolStop(warningSymbol, text);
113 +
	}
114 +
115 +
	info(text) {
116 +
		return this.#symbolStop(infoSymbol, text);
117 +
	}
118 +
119 +
	get isSpinning() {
120 +
		return this.#isSpinning;
121 +
	}
122 +
123 +
	get text() {
124 +
		return this.#text;
125 +
	}
126 +
127 +
	set text(value) {
128 +
		this.#text = value ?? "";
129 +
		this.#render();
130 +
	}
131 +
132 +
	get color() {
133 +
		return this.#color;
134 +
	}
135 +
136 +
	set color(value) {
137 +
		this.#color = value;
138 +
		this.#render();
139 +
	}
140 +
141 +
	clear() {
142 +
		if (!this.#isInteractive) {
143 +
			return this;
144 +
		}
145 +
146 +
		this.#stream.cursorTo(0);
147 +
148 +
		for (let index = 0; index < this.#lines; index++) {
149 +
			if (index > 0) {
150 +
				this.#stream.moveCursor(0, -1);
151 +
			}
152 +
153 +
			this.#stream.clearLine(1);
154 +
		}
155 +
156 +
		this.#lines = 0;
157 +
158 +
		return this;
159 +
	}
160 +
161 +
	#render() {
162 +
		// Ensure we only update the spinner frame at the wanted interval,
163 +
		// even if the frame method is called more often.
164 +
		const now = Date.now();
165 +
		if (
166 +
			this.#currentFrame === -1 ||
167 +
			now - this.#lastSpinnerFrameTime >= this.#interval
168 +
		) {
169 +
			this.#currentFrame = ++this.#currentFrame % this.#frames.length;
170 +
			this.#lastSpinnerFrameTime = now;
171 +
		}
172 +
173 +
		const applyColor = yoctocolors[this.#color] ?? yoctocolors.cyan;
174 +
		const frame = this.#frames[this.#currentFrame];
175 +
		let string = `${applyColor(frame)} ${this.#text}`;
176 +
177 +
		if (!this.#isInteractive) {
178 +
			string += "\n";
179 +
		}
180 +
181 +
		this.clear();
182 +
		this.#write(string);
183 +
184 +
		if (this.#isInteractive) {
185 +
			this.#lines = this.#lineCount(string);
186 +
		}
187 +
	}
188 +
189 +
	#write(text) {
190 +
		this.#stream.write(text);
191 +
	}
192 +
193 +
	#lineCount(text) {
194 +
		const width = this.#stream.columns ?? 80;
195 +
		const lines = stripVTControlCharacters(text).split("\n");
196 +
197 +
		let lineCount = 0;
198 +
		for (const line of lines) {
199 +
			lineCount += Math.max(1, Math.ceil(line.length / width));
200 +
		}
201 +
202 +
		return lineCount;
203 +
	}
204 +
205 +
	#hideCursor() {
206 +
		if (this.#isInteractive) {
207 +
			this.#write("\u001B[?25l");
208 +
		}
209 +
	}
210 +
211 +
	#showCursor() {
212 +
		if (this.#isInteractive) {
213 +
			this.#write("\u001B[?25h");
214 +
		}
215 +
	}
216 +
217 +
	#subscribeToProcessEvents() {
218 +
		process.once("SIGINT", this.#exitHandlerBound);
219 +
		process.once("SIGTERM", this.#exitHandlerBound);
220 +
	}
221 +
222 +
	#unsubscribeFromProcessEvents() {
223 +
		process.off("SIGINT", this.#exitHandlerBound);
224 +
		process.off("SIGTERM", this.#exitHandlerBound);
225 +
	}
226 +
227 +
	#exitHandler(signal) {
228 +
		if (this.isSpinning) {
229 +
			this.stop();
230 +
		}
231 +
232 +
		// SIGINT: 128 + 2
233 +
		// SIGTERM: 128 + 15
234 +
		const exitCode = signal === "SIGINT" ? 130 : signal === "SIGTERM" ? 143 : 1;
235 +
		process.exit(exitCode);
236 +
	}
237 +
}
238 +
239 +
export default function yoctoSpinner(options) {
240 +
	return new YoctoSpinner(options);
241 +
}