@unscientificjszhai/howto 1.0.0-alpha.2 → 1.0.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.
@@ -1,80 +1,59 @@
1
- import React, { useState, useMemo } from "react";
1
+ import React, { useState } from "react";
2
2
  import { Box, Text, useInput } from "ink";
3
3
  import { SelectedCommandDisplay } from "./SelectedCommandDisplay.js";
4
- export const ResolvePlaceholdersView = ({ candidate, placeholders, onResolve, onBack, onCancel, }) => {
5
- const [activeIndex, setActiveIndex] = useState(0);
6
- const [values, setValues] = useState(placeholders.map(() => ""));
7
- const [buffer, setBuffer] = useState("");
8
- const resolvedValuesMap = useMemo(() => {
9
- const map = new Map();
10
- placeholders.forEach((p, i) => {
11
- if (i < activeIndex) {
12
- map.set(p.name, values[i]);
13
- }
14
- });
15
- return map;
16
- }, [placeholders, activeIndex, values]);
17
- const currentBuffer = useMemo(() => ({
18
- name: placeholders[activeIndex].name,
19
- value: buffer,
20
- }), [placeholders, activeIndex, buffer]);
4
+ import { applyPlaceholderResolutionInput, createPlaceholderResolution, getPlaceholderResolutionView, } from "./placeholder-logic.js";
5
+ export const ResolvePlaceholdersView = ({ candidate, onResolve, onBack, onCancel, }) => {
6
+ const [resolutionState, setResolutionState] = useState(() => createPlaceholderResolution(candidate));
7
+ const { currentPlaceholder, currentBuffer, resolvedValues } = getPlaceholderResolutionView(resolutionState);
21
8
  useInput((input, key) => {
22
9
  if (key.ctrl && input === "c") {
23
10
  onCancel();
24
11
  return;
25
12
  }
26
- if (key.escape) {
27
- if (activeIndex === 0) {
28
- onBack();
29
- }
30
- else {
31
- const newValues = [...values];
32
- newValues[activeIndex] = "";
33
- setValues(newValues);
34
- setActiveIndex((prev) => prev - 1);
35
- setBuffer(values[activeIndex - 1]);
36
- }
37
- return;
38
- }
39
- if (key.return) {
40
- const newValues = [...values];
41
- newValues[activeIndex] = buffer;
42
- setValues(newValues);
43
- if (activeIndex === placeholders.length - 1) {
44
- const resultMap = new Map();
45
- placeholders.forEach((p, i) => {
46
- resultMap.set(p.name, newValues[i]);
47
- });
48
- onResolve(resultMap);
49
- }
50
- else {
51
- setActiveIndex((prev) => prev + 1);
52
- setBuffer(values[activeIndex + 1]);
53
- }
13
+ const resolutionInput = toPlaceholderResolutionInput(input, key);
14
+ if (!resolutionInput) {
54
15
  return;
55
16
  }
56
- if (key.backspace || key.delete) {
57
- setBuffer((prev) => prev.slice(0, -1));
58
- return;
59
- }
60
- if (input && !Object.values(key).some(Boolean)) {
61
- setBuffer((prev) => prev + input);
17
+ const transition = applyPlaceholderResolutionInput(resolutionState, resolutionInput);
18
+ switch (transition.type) {
19
+ case "editing":
20
+ setResolutionState(transition.state);
21
+ return;
22
+ case "back-to-selection":
23
+ onBack();
24
+ return;
25
+ case "complete":
26
+ onResolve(transition.resolved);
27
+ return;
62
28
  }
63
- // Handle space and other printable characters that might be in key but not in input if needed
64
- // However ink's useInput input is usually the string.
65
29
  });
66
30
  return (React.createElement(Box, { flexDirection: "column", marginY: 1 },
67
- React.createElement(SelectedCommandDisplay, { candidate: candidate, resolvedValues: resolvedValuesMap, currentBuffer: currentBuffer }),
31
+ React.createElement(SelectedCommandDisplay, { candidate: candidate, resolvedValues: resolvedValues, currentBuffer: currentBuffer }),
68
32
  React.createElement(Text, { color: "green" }, "? Fill command placeholders"),
69
33
  React.createElement(Box, { flexDirection: "column", marginTop: 1 },
70
34
  React.createElement(Box, { flexDirection: "column", marginBottom: 1 },
71
35
  React.createElement(Text, null,
72
- placeholders[activeIndex].name,
36
+ currentPlaceholder.name,
73
37
  ": ",
74
- placeholders[activeIndex].description),
38
+ currentPlaceholder.description),
75
39
  React.createElement(Box, null,
76
40
  React.createElement(Text, { color: "cyan" }, "> "),
77
- React.createElement(Text, null, buffer),
41
+ React.createElement(Text, null, currentBuffer.value),
78
42
  React.createElement(Text, { backgroundColor: "white" }, " ")))),
79
43
  React.createElement(Text, { dimColor: true }, "Press Enter for next value, Esc to go back, Ctrl+C to cancel.")));
80
44
  };
45
+ function toPlaceholderResolutionInput(input, key) {
46
+ if (key.escape) {
47
+ return { type: "escape" };
48
+ }
49
+ if (key.return) {
50
+ return { type: "commit" };
51
+ }
52
+ if (key.backspace || key.delete) {
53
+ return { type: "delete" };
54
+ }
55
+ if (input && !Object.values(key).some(Boolean)) {
56
+ return { type: "append", value: input };
57
+ }
58
+ return undefined;
59
+ }
@@ -5,6 +5,64 @@ export class PlaceholderResolutionError extends Error {
5
5
  this.name = "PlaceholderResolutionError";
6
6
  }
7
7
  }
8
+ export function createPlaceholderResolution(candidate) {
9
+ if (candidate.placeholders.length === 0) {
10
+ throw new PlaceholderResolutionError("cannot resolve command without placeholders");
11
+ }
12
+ return {
13
+ candidate,
14
+ activeIndex: 0,
15
+ values: candidate.placeholders.map(() => ""),
16
+ buffer: "",
17
+ };
18
+ }
19
+ export function getPlaceholderResolutionView(state) {
20
+ const currentPlaceholder = state.candidate.placeholders[state.activeIndex];
21
+ if (!currentPlaceholder) {
22
+ throw new PlaceholderResolutionError("placeholder resolution state is out of bounds");
23
+ }
24
+ const resolvedValues = new Map();
25
+ state.candidate.placeholders.forEach((placeholder, index) => {
26
+ if (index < state.activeIndex) {
27
+ resolvedValues.set(placeholder.name, state.values[index]);
28
+ }
29
+ });
30
+ return {
31
+ currentPlaceholder,
32
+ currentBuffer: {
33
+ name: currentPlaceholder.name,
34
+ value: state.buffer,
35
+ },
36
+ resolvedValues,
37
+ };
38
+ }
39
+ export function applyPlaceholderResolutionInput(state, input) {
40
+ switch (input.type) {
41
+ case "append":
42
+ return {
43
+ type: "editing",
44
+ state: Object.assign(Object.assign({}, state), { buffer: state.buffer + input.value }),
45
+ };
46
+ case "delete":
47
+ return {
48
+ type: "editing",
49
+ state: Object.assign(Object.assign({}, state), { buffer: state.buffer.slice(0, -1) }),
50
+ };
51
+ case "escape":
52
+ return applyEscape(state);
53
+ case "commit":
54
+ return applyCommit(state);
55
+ }
56
+ }
57
+ export function resolveCandidatePlaceholders(candidate, values) {
58
+ const command = replaceCommandPlaceholders(candidate.command, values);
59
+ assertNoUnresolvedPlaceholders(command);
60
+ return {
61
+ candidate,
62
+ values,
63
+ command,
64
+ };
65
+ }
8
66
  export function replaceCommandPlaceholders(command, values) {
9
67
  let resolvedCommand = command;
10
68
  for (const [name, value] of values) {
@@ -17,3 +75,34 @@ export function assertNoUnresolvedPlaceholders(command) {
17
75
  throw new PlaceholderResolutionError("final command contains unresolved placeholders");
18
76
  }
19
77
  }
78
+ function applyEscape(state) {
79
+ if (state.activeIndex === 0) {
80
+ return { type: "back-to-selection" };
81
+ }
82
+ const previousIndex = state.activeIndex - 1;
83
+ const values = [...state.values];
84
+ values[state.activeIndex] = "";
85
+ return {
86
+ type: "editing",
87
+ state: Object.assign(Object.assign({}, state), { activeIndex: previousIndex, values, buffer: values[previousIndex] }),
88
+ };
89
+ }
90
+ function applyCommit(state) {
91
+ const values = [...state.values];
92
+ values[state.activeIndex] = state.buffer;
93
+ if (state.activeIndex === state.candidate.placeholders.length - 1) {
94
+ const resolvedValues = new Map();
95
+ state.candidate.placeholders.forEach((placeholder, index) => {
96
+ resolvedValues.set(placeholder.name, values[index]);
97
+ });
98
+ return {
99
+ type: "complete",
100
+ resolved: resolveCandidatePlaceholders(state.candidate, resolvedValues),
101
+ };
102
+ }
103
+ const nextIndex = state.activeIndex + 1;
104
+ return {
105
+ type: "editing",
106
+ state: Object.assign(Object.assign({}, state), { activeIndex: nextIndex, values, buffer: values[nextIndex] }),
107
+ };
108
+ }
@@ -0,0 +1,19 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
10
+ import { parseAndValidateAiResponse } from "./ai-response.js";
11
+ import { validateUseCommandCandidates } from "./command-tool.js";
12
+ export function generateValidatedCommandCandidates(provider, request) {
13
+ return __awaiter(this, void 0, void 0, function* () {
14
+ const result = yield provider.generateCommands(request);
15
+ const response = parseAndValidateAiResponse(result.rawText);
16
+ validateUseCommandCandidates(response, request.useCommand);
17
+ return response.commands;
18
+ });
19
+ }
package/package.json CHANGED
@@ -1,14 +1,24 @@
1
1
  {
2
2
  "name": "@unscientificjszhai/howto",
3
- "version": "1.0.0-alpha.2",
3
+ "version": "1.0.0",
4
4
  "type": "module",
5
5
  "description": "Use AI to quickly find commands within the terminal.",
6
+ "license": "MIT",
6
7
  "main": "dist/index.js",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/UnscientificJsZhai/HowTo.git"
11
+ },
12
+ "publishConfig": {
13
+ "access": "public",
14
+ "registry": "https://registry.npmjs.org/"
15
+ },
7
16
  "bin": {
8
17
  "howto": "dist/index.js"
9
18
  },
10
19
  "files": [
11
- "dist"
20
+ "dist",
21
+ "THIRD_PARTY_NOTICES.md"
12
22
  ],
13
23
  "scripts": {
14
24
  "build": "tsc",
@@ -22,10 +32,9 @@
22
32
  "test:unit": "tsc -p tsconfig.test.json && node --test dist-test/tests/unit/*.test.js dist-test/tests/unit/**/*.test.js"
23
33
  },
24
34
  "dependencies": {
25
- "@google/genai": "^1.0.0",
26
- "@inquirer/prompts": "^7.0.0",
27
- "ink": "^7.0.3",
28
- "openai": "^4.0.0",
35
+ "@google/genai": "^2.6.0",
36
+ "ink": "^7.0.4",
37
+ "openai": "^6.39.0",
29
38
  "react": "^19.2.6"
30
39
  },
31
40
  "devDependencies": {
package/dist/env.js DELETED
@@ -1,10 +0,0 @@
1
- import os from "os";
2
- export function getTerminalEnv(env = process.env) {
3
- return {
4
- os: os.platform(),
5
- shell: env.SHELL || (os.platform() === "win32" ? "cmd.exe" : "sh"),
6
- };
7
- }
8
- export function isZsh(shell) {
9
- return shell.toLowerCase().endsWith("zsh");
10
- }
@@ -1,117 +0,0 @@
1
- "use strict";
2
- var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
- return new (P || (P = Promise))(function (resolve, reject) {
5
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
- step((generator = generator.apply(thisArg, _arguments || [])).next());
9
- });
10
- };
11
- var __importDefault = (this && this.__importDefault) || function (mod) {
12
- return (mod && mod.__esModule) ? mod : { "default": mod };
13
- };
14
- Object.defineProperty(exports, "__esModule", { value: true });
15
- exports.confirmFinalCommand = confirmFinalCommand;
16
- exports.confirmDangerousCommand = confirmDangerousCommand;
17
- exports.formatFinalCommandConfirmation = formatFinalCommandConfirmation;
18
- exports.formatDangerousCommandConfirmation = formatDangerousCommandConfirmation;
19
- const readline_1 = __importDefault(require("readline"));
20
- const interactive_1 = require("./interactive");
21
- const key_input_1 = require("./key-input");
22
- function confirmFinalCommand(command_1) {
23
- return __awaiter(this, arguments, void 0, function* (command, streams = {}) {
24
- var _a, _b;
25
- const input = (_a = streams.input) !== null && _a !== void 0 ? _a : process.stdin;
26
- const output = (_b = streams.output) !== null && _b !== void 0 ? _b : process.stdout;
27
- (0, interactive_1.ensureInteractiveTty)(input, output);
28
- return new Promise((resolve, reject) => {
29
- let cleanupKeypressSession;
30
- const cleanup = () => {
31
- cleanupKeypressSession === null || cleanupKeypressSession === void 0 ? void 0 : cleanupKeypressSession();
32
- cleanupKeypressSession = undefined;
33
- };
34
- const finish = () => {
35
- cleanup();
36
- resolve();
37
- };
38
- const cancel = () => {
39
- cleanup();
40
- reject(new interactive_1.InteractionCancelledError("Final command confirmation cancelled"));
41
- };
42
- const onKeypress = (_inputText, key = {}) => {
43
- if (key.ctrl === true && key.name === "c") {
44
- cancel();
45
- return;
46
- }
47
- if (key.name === "escape") {
48
- cancel();
49
- return;
50
- }
51
- if (key.name === "return" || key.name === "enter") {
52
- finish();
53
- }
54
- };
55
- output.write(formatFinalCommandConfirmation(command));
56
- cleanupKeypressSession = (0, key_input_1.beginKeypressSession)(input, onKeypress);
57
- });
58
- });
59
- }
60
- function confirmDangerousCommand(command_1, match_1) {
61
- return __awaiter(this, arguments, void 0, function* (command, match, streams = {}) {
62
- var _a, _b;
63
- const input = (_a = streams.input) !== null && _a !== void 0 ? _a : process.stdin;
64
- const output = (_b = streams.output) !== null && _b !== void 0 ? _b : process.stdout;
65
- (0, interactive_1.ensureInteractiveTty)(input, output);
66
- const rl = readline_1.default.createInterface({
67
- input,
68
- output,
69
- terminal: input.isTTY === true && output.isTTY === true,
70
- });
71
- try {
72
- output.write(formatDangerousCommandConfirmation(command, match));
73
- const answer = yield readConfirmationLine(rl, input);
74
- if (answer !== "EXECUTE") {
75
- throw new interactive_1.InteractionCancelledError("Dangerous command confirmation cancelled");
76
- }
77
- }
78
- finally {
79
- rl.close();
80
- }
81
- });
82
- }
83
- function formatFinalCommandConfirmation(command) {
84
- return `\nFinal command:\n${command}\n\nPress Enter to execute, Esc or Ctrl+C to cancel.\n`;
85
- }
86
- function formatDangerousCommandConfirmation(command, match) {
87
- return [
88
- "\nDangerous command detected.",
89
- `Rule: ${match.rule}`,
90
- `Risk: ${match.reason}`,
91
- "",
92
- "Final command:",
93
- command,
94
- "",
95
- 'Type EXECUTE to continue, or anything else to cancel.',
96
- "> ",
97
- ].join("\n");
98
- }
99
- function readConfirmationLine(rl, input) {
100
- input.resume();
101
- return new Promise((resolve, reject) => {
102
- const cleanup = () => {
103
- rl.off("line", onLine);
104
- rl.off("SIGINT", onSigint);
105
- };
106
- const onLine = (line) => {
107
- cleanup();
108
- resolve(line);
109
- };
110
- const onSigint = () => {
111
- cleanup();
112
- reject(new interactive_1.InteractionCancelledError("Dangerous command confirmation cancelled"));
113
- };
114
- rl.on("line", onLine);
115
- rl.on("SIGINT", onSigint);
116
- });
117
- }
@@ -1,115 +0,0 @@
1
- "use strict";
2
- var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
- return new (P || (P = Promise))(function (resolve, reject) {
5
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
- step((generator = generator.apply(thisArg, _arguments || [])).next());
9
- });
10
- };
11
- Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.InteractionCancelledError = exports.InteractiveTtyError = void 0;
13
- exports.selectCommandCandidate = selectCommandCandidate;
14
- exports.ensureInteractiveTty = ensureInteractiveTty;
15
- exports.formatCandidateSelection = formatCandidateSelection;
16
- const key_input_1 = require("./key-input");
17
- class InteractiveTtyError extends Error {
18
- constructor(message = "Interactive mode requires a TTY. Run howto in an interactive terminal or use --print.") {
19
- super(message);
20
- this.name = "InteractiveTtyError";
21
- }
22
- }
23
- exports.InteractiveTtyError = InteractiveTtyError;
24
- class InteractionCancelledError extends Error {
25
- constructor(message = "Interactive selection cancelled") {
26
- super(message);
27
- this.name = "InteractionCancelledError";
28
- }
29
- }
30
- exports.InteractionCancelledError = InteractionCancelledError;
31
- function selectCommandCandidate(candidates_1) {
32
- return __awaiter(this, arguments, void 0, function* (candidates, streams = {}) {
33
- var _a, _b;
34
- const input = (_a = streams.input) !== null && _a !== void 0 ? _a : process.stdin;
35
- const output = (_b = streams.output) !== null && _b !== void 0 ? _b : process.stdout;
36
- ensureInteractiveTty(input, output);
37
- return new Promise((resolve, reject) => {
38
- let activeIndex = 0;
39
- let renderedLineCount = 0;
40
- let cleanupKeypressSession;
41
- const clearRenderedSelection = () => {
42
- if (renderedLineCount > 0) {
43
- output.write(`\x1B[${renderedLineCount}A\x1B[J`);
44
- renderedLineCount = 0;
45
- }
46
- };
47
- const cleanup = () => {
48
- cleanupKeypressSession === null || cleanupKeypressSession === void 0 ? void 0 : cleanupKeypressSession();
49
- cleanupKeypressSession = undefined;
50
- clearRenderedSelection();
51
- output.write("\x1B[?25h");
52
- };
53
- const finish = (candidate) => {
54
- cleanup();
55
- resolve(candidate);
56
- };
57
- const cancel = () => {
58
- cleanup();
59
- reject(new InteractionCancelledError());
60
- };
61
- const render = () => {
62
- const text = formatCandidateSelection(candidates, activeIndex);
63
- clearRenderedSelection();
64
- output.write(`\x1B[?25l${text}\n`);
65
- renderedLineCount = text.split("\n").length + 1;
66
- };
67
- const onKeypress = (_inputText, key = {}) => {
68
- if (key.ctrl === true && key.name === "c") {
69
- cancel();
70
- return;
71
- }
72
- if (key.name === "escape") {
73
- cancel();
74
- return;
75
- }
76
- if (key.name === "return" || key.name === "enter") {
77
- finish(candidates[activeIndex]);
78
- return;
79
- }
80
- if (key.name === "up") {
81
- activeIndex = (activeIndex - 1 + candidates.length) % candidates.length;
82
- render();
83
- return;
84
- }
85
- if (key.name === "down") {
86
- activeIndex = (activeIndex + 1) % candidates.length;
87
- render();
88
- }
89
- };
90
- cleanupKeypressSession = (0, key_input_1.beginKeypressSession)(input, onKeypress);
91
- render();
92
- });
93
- });
94
- }
95
- function ensureInteractiveTty(input, output) {
96
- if (input.isTTY !== true || output.isTTY !== true) {
97
- throw new InteractiveTtyError();
98
- }
99
- }
100
- function formatCandidateSelection(candidates, activeIndex) {
101
- const lines = ["? Select a command", ""];
102
- candidates.forEach((candidate, index) => {
103
- const prefix = index === activeIndex ? ">" : " ";
104
- const titleLine = `${prefix} ${candidate.title}`;
105
- lines.push(index === activeIndex ? `\x1B[7m${titleLine}\x1B[0m` : titleLine);
106
- lines.push(` ${candidate.command}`);
107
- lines.push(` ${candidate.description}`);
108
- if (index < candidates.length - 1) {
109
- lines.push("");
110
- }
111
- });
112
- lines.push("");
113
- lines.push("Use Up/Down to move, Enter to select, Esc or Ctrl+C to cancel.");
114
- return lines.join("\n");
115
- }
@@ -1,23 +0,0 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.beginKeypressSession = beginKeypressSession;
7
- const readline_1 = __importDefault(require("readline"));
8
- function beginKeypressSession(input, onKeypress) {
9
- const wasRaw = input.isRaw === true;
10
- readline_1.default.emitKeypressEvents(input);
11
- input.on("keypress", onKeypress);
12
- if (typeof input.setRawMode === "function") {
13
- input.setRawMode(true);
14
- }
15
- input.resume();
16
- return () => {
17
- input.off("keypress", onKeypress);
18
- if (typeof input.setRawMode === "function" && !wasRaw) {
19
- input.setRawMode(false);
20
- }
21
- input.pause();
22
- };
23
- }
@@ -1,40 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.createAiLoadingIndicator = createAiLoadingIndicator;
4
- const FRAMES = ["|", "/", "-", "\\"];
5
- const MESSAGE = "Waiting for AI response...";
6
- function createAiLoadingIndicator(options) {
7
- var _a, _b;
8
- const output = (_a = options.output) !== null && _a !== void 0 ? _a : process.stdout;
9
- const intervalMs = (_b = options.intervalMs) !== null && _b !== void 0 ? _b : 120;
10
- let timer;
11
- let frameIndex = 0;
12
- let running = false;
13
- const shouldRender = () => options.enabled && output.isTTY === true;
14
- const render = () => {
15
- const frame = FRAMES[frameIndex % FRAMES.length];
16
- frameIndex += 1;
17
- output.write(`\r${frame} ${MESSAGE}`);
18
- };
19
- return {
20
- start() {
21
- if (!shouldRender() || running) {
22
- return;
23
- }
24
- running = true;
25
- render();
26
- timer = setInterval(render, intervalMs);
27
- },
28
- stop() {
29
- if (!running) {
30
- return;
31
- }
32
- if (timer !== undefined) {
33
- clearInterval(timer);
34
- timer = undefined;
35
- }
36
- running = false;
37
- output.write("\r\x1B[2K");
38
- },
39
- };
40
- }