jira-pilot 2.1.2 → 2.1.3

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.
Files changed (107) hide show
  1. package/README.md +23 -0
  2. package/bin/jira.ts +2 -0
  3. package/dist/bin/jira.js +2 -0
  4. package/dist/bin/jira.js.map +1 -1
  5. package/dist/src/commands/ai-actions/plan.js +1 -1
  6. package/dist/src/commands/ai-actions/plan.js.map +1 -1
  7. package/dist/src/commands/ai-actions/review.js +5 -4
  8. package/dist/src/commands/ai-actions/review.js.map +1 -1
  9. package/dist/src/commands/ai-actions/standup.js +1 -1
  10. package/dist/src/commands/ai-actions/standup.js.map +1 -1
  11. package/dist/src/commands/ai.js +1 -1
  12. package/dist/src/commands/ai.js.map +1 -1
  13. package/dist/src/commands/board.js +10 -5
  14. package/dist/src/commands/board.js.map +1 -1
  15. package/dist/src/commands/bulk.js +1 -1
  16. package/dist/src/commands/bulk.js.map +1 -1
  17. package/dist/src/commands/config.js +1 -1
  18. package/dist/src/commands/config.js.map +1 -1
  19. package/dist/src/commands/dashboard.js +11 -5
  20. package/dist/src/commands/dashboard.js.map +1 -1
  21. package/dist/src/commands/filter.js +7 -4
  22. package/dist/src/commands/filter.js.map +1 -1
  23. package/dist/src/commands/git.js +1 -1
  24. package/dist/src/commands/git.js.map +1 -1
  25. package/dist/src/commands/issue-attach.js +1 -1
  26. package/dist/src/commands/issue-attach.js.map +1 -1
  27. package/dist/src/commands/issue-pr.js +1 -1
  28. package/dist/src/commands/issue-pr.js.map +1 -1
  29. package/dist/src/commands/issue-worklog.js +10 -5
  30. package/dist/src/commands/issue-worklog.js.map +1 -1
  31. package/dist/src/commands/issue.js +142 -87
  32. package/dist/src/commands/issue.js.map +1 -1
  33. package/dist/src/commands/project.js +10 -5
  34. package/dist/src/commands/project.js.map +1 -1
  35. package/dist/src/commands/sprint.js +19 -8
  36. package/dist/src/commands/sprint.js.map +1 -1
  37. package/dist/src/commands/tui.d.ts +2 -0
  38. package/dist/src/commands/tui.js +10 -0
  39. package/dist/src/commands/tui.js.map +1 -0
  40. package/dist/src/services/ai-service.js +7 -4
  41. package/dist/src/services/ai-service.js.map +1 -1
  42. package/dist/src/services/api-service.d.ts +2 -0
  43. package/dist/src/services/api-service.js +29 -20
  44. package/dist/src/services/api-service.js.map +1 -1
  45. package/dist/src/tui/App.d.ts +1 -0
  46. package/dist/src/tui/App.js +26 -0
  47. package/dist/src/tui/App.js.map +1 -0
  48. package/dist/src/tui/index.d.ts +1 -0
  49. package/dist/src/tui/index.js +8 -0
  50. package/dist/src/tui/index.js.map +1 -0
  51. package/dist/src/tui/screens/BoardList.d.ts +1 -0
  52. package/dist/src/tui/screens/BoardList.js +71 -0
  53. package/dist/src/tui/screens/BoardList.js.map +1 -0
  54. package/dist/src/tui/screens/Dashboard.d.ts +1 -0
  55. package/dist/src/tui/screens/Dashboard.js +41 -0
  56. package/dist/src/tui/screens/Dashboard.js.map +1 -0
  57. package/dist/src/tui/screens/IssueDetail.d.ts +6 -0
  58. package/dist/src/tui/screens/IssueDetail.js +40 -0
  59. package/dist/src/tui/screens/IssueDetail.js.map +1 -0
  60. package/dist/src/tui/screens/IssueList.d.ts +1 -0
  61. package/dist/src/tui/screens/IssueList.js +72 -0
  62. package/dist/src/tui/screens/IssueList.js.map +1 -0
  63. package/dist/src/tui/screens/KanbanBoard.d.ts +6 -0
  64. package/dist/src/tui/screens/KanbanBoard.js +86 -0
  65. package/dist/src/tui/screens/KanbanBoard.js.map +1 -0
  66. package/dist/src/tui/utils/adf-render.d.ts +1 -0
  67. package/dist/src/tui/utils/adf-render.js +29 -0
  68. package/dist/src/tui/utils/adf-render.js.map +1 -0
  69. package/dist/src/utils/error-handler.d.ts +2 -2
  70. package/dist/src/utils/error-handler.js.map +1 -1
  71. package/dist/src/utils/http.d.ts +27 -0
  72. package/dist/src/utils/http.js +95 -0
  73. package/dist/src/utils/http.js.map +1 -0
  74. package/dist/src/utils/spinner.d.ts +21 -0
  75. package/dist/src/utils/spinner.js +79 -0
  76. package/dist/src/utils/spinner.js.map +1 -0
  77. package/package.json +10 -5
  78. package/src/commands/ai-actions/plan.ts +1 -1
  79. package/src/commands/ai-actions/review.ts +5 -4
  80. package/src/commands/ai-actions/standup.ts +1 -1
  81. package/src/commands/ai.ts +1 -1
  82. package/src/commands/board.ts +10 -5
  83. package/src/commands/bulk.ts +1 -1
  84. package/src/commands/config.ts +1 -1
  85. package/src/commands/dashboard.ts +11 -5
  86. package/src/commands/filter.ts +8 -5
  87. package/src/commands/git.ts +1 -1
  88. package/src/commands/issue-attach.ts +1 -1
  89. package/src/commands/issue-pr.ts +1 -1
  90. package/src/commands/issue-worklog.ts +10 -5
  91. package/src/commands/issue.ts +149 -89
  92. package/src/commands/project.ts +10 -5
  93. package/src/commands/sprint.ts +19 -8
  94. package/src/commands/tui.ts +11 -0
  95. package/src/services/ai-service.ts +7 -4
  96. package/src/services/api-service.ts +29 -21
  97. package/src/tui/App.tsx +61 -0
  98. package/src/tui/index.tsx +8 -0
  99. package/src/tui/screens/BoardList.tsx +102 -0
  100. package/src/tui/screens/Dashboard.tsx +75 -0
  101. package/src/tui/screens/IssueDetail.tsx +93 -0
  102. package/src/tui/screens/IssueList.tsx +116 -0
  103. package/src/tui/screens/KanbanBoard.tsx +133 -0
  104. package/src/tui/utils/adf-render.ts +30 -0
  105. package/src/utils/error-handler.ts +2 -2
  106. package/src/utils/http.ts +128 -0
  107. package/src/utils/spinner.ts +87 -0
@@ -0,0 +1,116 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import { Box, Text, useInput } from 'ink';
3
+ import SpinnerOriginal from 'ink-spinner';
4
+ const Spinner = SpinnerOriginal as any;
5
+ import { api } from '../../services/api-service.js';
6
+ import IssueDetail from './IssueDetail.js';
7
+
8
+ export default function IssueList() {
9
+ const [loading, setLoading] = useState(true);
10
+ const [issues, setIssues] = useState<any[]>([]);
11
+ const [selectedIndex, setSelectedIndex] = useState(0);
12
+ const [selectedIssueKey, setSelectedIssueKey] = useState<string | null>(null);
13
+ const [error, setError] = useState<string | null>(null);
14
+
15
+ useEffect(() => {
16
+ const fetchIssues = async () => {
17
+ try {
18
+ // Default search: Assigned to me or reported by me, unresolved
19
+ const results = await api.search('assignee = currentUser() OR reporter = currentUser() AND resolution = Unresolved order by updated DESC', 0, 20);
20
+ setIssues(results.issues);
21
+ setLoading(false);
22
+ } catch (err: any) {
23
+ setError(err.message);
24
+ setLoading(false);
25
+ }
26
+ };
27
+ fetchIssues();
28
+ }, []);
29
+
30
+ useInput((input, key) => {
31
+ if (selectedIssueKey) return; // Let Detail view handle input if active
32
+
33
+ if (key.upArrow) {
34
+ setSelectedIndex((prev: number) => Math.max(0, prev - 1));
35
+ }
36
+ if (key.downArrow) {
37
+ setSelectedIndex((prev: number) => Math.min(issues.length - 1, prev + 1));
38
+ }
39
+ if (key.return) {
40
+ if (issues[selectedIndex]) {
41
+ setSelectedIssueKey(issues[selectedIndex].key);
42
+ }
43
+ }
44
+ });
45
+
46
+ if (selectedIssueKey) {
47
+ return <IssueDetail issueKey={selectedIssueKey} onBack={() => setSelectedIssueKey(null)} />;
48
+ }
49
+
50
+ if (loading) {
51
+ return (
52
+ <Box>
53
+ <Text color="green"><Spinner type="dots" /></Text>
54
+ <Text> Loading Issues...</Text>
55
+ </Box>
56
+ );
57
+ }
58
+ // ... rest of the file ...
59
+
60
+ if (error) {
61
+ return <Text color="red">Error: {error}</Text>;
62
+ }
63
+
64
+ if (issues.length === 0) {
65
+ return <Text>No issues found.</Text>;
66
+ }
67
+
68
+ // Pagination/Windowing logic could go here, for now just slice a window around selected
69
+ const windowSize = 10;
70
+ const start = Math.max(0, selectedIndex - Math.floor(windowSize / 2));
71
+ const end = Math.min(issues.length, start + windowSize);
72
+ const visibleIssues = issues.slice(start, end);
73
+
74
+ return (
75
+ <Box flexDirection="column">
76
+ <Box marginBottom={1}>
77
+ <Text bold underline>Issue Navigator ({issues.length})</Text>
78
+ </Box>
79
+
80
+ {visibleIssues.map((issue: any, index: number) => {
81
+ const globalIndex = start + index;
82
+ const isSelected = globalIndex === selectedIndex;
83
+ const key = issue.key;
84
+ const summary = issue.fields.summary;
85
+ const status = issue.fields.status.name;
86
+ const priority = issue.fields.priority.name;
87
+
88
+ return (
89
+ <Box key={issue.id}>
90
+ <Text color={isSelected ? 'green' : 'white'} bold={isSelected}>
91
+ {isSelected ? '> ' : ' '}
92
+ </Text>
93
+ <Box width={12}>
94
+ <Text color="cyan">{key}</Text>
95
+ </Box>
96
+ <Box width={12}>
97
+ <Text color="yellow">{status}</Text>
98
+ </Box>
99
+ <Box width={10}>
100
+ <Text color="magenta">{priority}</Text>
101
+ </Box>
102
+ <Box flexGrow={1}>
103
+ <Text wrap="truncate-end">{summary}</Text>
104
+ </Box>
105
+ </Box>
106
+ );
107
+ })}
108
+
109
+ <Box marginTop={1} borderStyle="single" borderColor="gray">
110
+ <Text color="gray">
111
+ Selected: {issues[selectedIndex]?.key} - {issues[selectedIndex]?.fields?.summary}
112
+ </Text>
113
+ </Box>
114
+ </Box>
115
+ );
116
+ }
@@ -0,0 +1,133 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import { Box, Text, useInput } from 'ink';
3
+ import SpinnerOriginal from 'ink-spinner';
4
+ const Spinner = SpinnerOriginal as any;
5
+ import { api } from '../../services/api-service.js';
6
+
7
+ interface KanbanBoardProps {
8
+ boardId: string;
9
+ onBack: () => void;
10
+ }
11
+
12
+ export default function KanbanBoard({ boardId, onBack }: KanbanBoardProps) {
13
+ const [loading, setLoading] = useState(true);
14
+ const [columns, setColumns] = useState<any[]>([]);
15
+ const [issues, setIssues] = useState<Record<string, any[]>>({});
16
+ const [activeColumnIndex, setActiveColumnIndex] = useState(0);
17
+ const [error, setError] = useState<string | null>(null);
18
+
19
+ useEffect(() => {
20
+ const fetchData = async () => {
21
+ try {
22
+ // parallel fetch
23
+ const [config, boardIssues] = await Promise.all([
24
+ api.agileGet(`/board/${boardId}/configuration`),
25
+ api.agileGet(`/board/${boardId}/issue`)
26
+ ]);
27
+
28
+ const cols = config.columnConfig.columns;
29
+ setColumns(cols);
30
+
31
+ // Group issues by status/column
32
+ // This is simplified mapping logic. Jira statuses map to columns.
33
+ const issuesByStatus: Record<string, any[]> = {};
34
+ cols.forEach((col: any) => {
35
+ issuesByStatus[col.name] = [];
36
+ });
37
+
38
+ boardIssues.issues.forEach((issue: any) => {
39
+ const statusName = issue.fields.status.name;
40
+ // Find which column this status belongs to involves checking config.columnConfig.columns[i].statuses
41
+ // For now, let's just try to match column name or push to 'Other'
42
+ let placed = false;
43
+ for (const col of cols) {
44
+ // Check if status in column statuses
45
+ // config.columnConfig.columns structure: { name: 'To Do', statuses: [ { id: '10000', self: '...' } ] }
46
+ if (col.statuses && col.statuses.some((s: any) => s.id === issue.fields.status.id)) {
47
+ if (!issuesByStatus[col.name]) issuesByStatus[col.name] = [];
48
+ issuesByStatus[col.name].push(issue);
49
+ placed = true;
50
+ break;
51
+ }
52
+ }
53
+ if (!placed) {
54
+ // fallback: try direct name match
55
+ if (issuesByStatus[statusName]) {
56
+ issuesByStatus[statusName].push(issue);
57
+ }
58
+ }
59
+ });
60
+
61
+ setIssues(issuesByStatus);
62
+ setLoading(false);
63
+ } catch (err: any) {
64
+ setError(err.message);
65
+ setLoading(false);
66
+ }
67
+ };
68
+ fetchData();
69
+ }, [boardId]);
70
+
71
+ useInput((input, key) => {
72
+ if (key.escape || input === 'b') {
73
+ onBack();
74
+ }
75
+ if (key.leftArrow) {
76
+ setActiveColumnIndex((prev: number) => Math.max(0, prev - 1));
77
+ }
78
+ if (key.rightArrow) {
79
+ setActiveColumnIndex((prev: number) => Math.min(columns.length - 1, prev + 1));
80
+ }
81
+ });
82
+
83
+ if (loading) {
84
+ return (
85
+ <Box>
86
+ <Text color="green"><Spinner type="dots" /></Text>
87
+ <Text> Loading Board...</Text>
88
+ </Box>
89
+ );
90
+ }
91
+
92
+ if (error) {
93
+ return <Text color="red">Error: {error}</Text>;
94
+ }
95
+
96
+ return (
97
+ <Box flexDirection="column" height="100%">
98
+ <Box marginBottom={1}>
99
+ <Text>Board View (Use ←/→ to switch columns, Esc to back)</Text>
100
+ </Box>
101
+
102
+ <Box flexDirection="row" flexGrow={1}>
103
+ {columns.map((col: any, index: number) => {
104
+ const isActive = index === activeColumnIndex;
105
+ const colIssues = issues[col.name] || [];
106
+
107
+ return (
108
+ <Box
109
+ key={col.name}
110
+ flexDirection="column"
111
+ width={30}
112
+ borderStyle={isActive ? 'double' : 'single'}
113
+ borderColor={isActive ? 'green' : 'gray'}
114
+ marginRight={1}
115
+ paddingX={1}
116
+ >
117
+ <Box marginBottom={1} borderStyle="single" borderBottom={false} borderLeft={false} borderRight={false} borderTop={false}>
118
+ <Text bold underline color={isActive ? 'green' : 'white'}>{col.name} ({colIssues.length})</Text>
119
+ </Box>
120
+
121
+ {colIssues.slice(0, 10).map((issue: any) => ( // Limit simplified list
122
+ <Box key={issue.id} marginBottom={1}>
123
+ <Text color="cyan">{issue.key}</Text>
124
+ <Text wrap="truncate">{issue.fields.summary}</Text>
125
+ </Box>
126
+ ))}
127
+ </Box>
128
+ );
129
+ })}
130
+ </Box>
131
+ </Box>
132
+ );
133
+ }
@@ -0,0 +1,30 @@
1
+
2
+ export function renderADF(node: any): string {
3
+ if (!node) return '';
4
+ if (typeof node === 'string') return node;
5
+
6
+ // Handle text nodes
7
+ if (node.type === 'text') {
8
+ return node.text || '';
9
+ }
10
+
11
+ // Handle content arrays
12
+ if (node.content && Array.isArray(node.content)) {
13
+ return node.content.map(renderADF).join('');
14
+ }
15
+
16
+ // Handle specific block types (optional formatting)
17
+ switch (node.type) {
18
+ case 'paragraph':
19
+ return renderADF({ content: node.content }) + '\n';
20
+ case 'bulletList':
21
+ case 'orderedList':
22
+ return renderADF({ content: node.content });
23
+ case 'listItem':
24
+ return '• ' + renderADF({ content: node.content });
25
+ case 'hardBreak':
26
+ return '\n';
27
+ default:
28
+ return '';
29
+ }
30
+ }
@@ -1,5 +1,5 @@
1
1
  import chalk from 'chalk';
2
- import { Ora } from 'ora';
2
+ import { Spinner } from './spinner.js';
3
3
 
4
4
  /**
5
5
  * Standardized error handler for CLI commands.
@@ -9,7 +9,7 @@ import { Ora } from 'ora';
9
9
  * @param {Error} error - The error object
10
10
  * @param {string} [context] - Optional context (e.g., "Failed to list issues")
11
11
  */
12
- export function handleCommandError(spinner: Ora | null, error: any, context = 'Operation failed') {
12
+ export function handleCommandError(spinner: Spinner | null, error: any, context = 'Operation failed') {
13
13
  // Handle user cancellation (Ctrl+C in enquirer)
14
14
  if (error === '' || (error && error.message === '')) {
15
15
  if (spinner) spinner.stop();
@@ -0,0 +1,128 @@
1
+
2
+ import https from 'https';
3
+
4
+ interface HelperConfig {
5
+ baseURL?: string;
6
+ headers?: Record<string, string>;
7
+ validateStatus?: (status: number) => boolean;
8
+ httpsAgent?: https.Agent;
9
+ }
10
+
11
+ interface RequestConfig extends HelperConfig {
12
+ params?: Record<string, any>;
13
+ }
14
+
15
+ interface Response<T = any> {
16
+ data: T;
17
+ status: number;
18
+ statusText: string;
19
+ headers: Record<string, string>;
20
+ }
21
+
22
+ export class HttpClient {
23
+ private defaults: HelperConfig;
24
+
25
+ constructor(defaults: HelperConfig = {}) {
26
+ this.defaults = defaults;
27
+ }
28
+
29
+ static create(config: HelperConfig = {}) {
30
+ return new HttpClient(config);
31
+ }
32
+
33
+ async request<T = any>(url: string, options: RequestInit & RequestConfig = {}): Promise<Response<T>> {
34
+ const baseURL = options.baseURL || this.defaults.baseURL || '';
35
+ const fullUrl = new URL(url.startsWith('http') ? url : `${baseURL}${url}`);
36
+
37
+ if (options.params) {
38
+ Object.entries(options.params).forEach(([key, value]) => {
39
+ if (value !== undefined) fullUrl.searchParams.append(key, String(value));
40
+ });
41
+ }
42
+
43
+ const headers = {
44
+ ...this.defaults.headers,
45
+ ...options.headers
46
+ } as Record<string, string>;
47
+
48
+ // Handle body for JSON
49
+ let body = options.body;
50
+ if (body && typeof body === 'object') {
51
+ const explicitContentType = headers['Content-Type'];
52
+ if (!explicitContentType || explicitContentType.includes('application/json')) {
53
+ if (!explicitContentType) {
54
+ headers['Content-Type'] = 'application/json';
55
+ }
56
+ body = JSON.stringify(body);
57
+ }
58
+ }
59
+
60
+ const fetchOptions: RequestInit = {
61
+ method: options.method || 'GET',
62
+ headers,
63
+ body: body as BodyInit,
64
+ // Node native fetch doesn't support 'agent' directly in standard RequestInit
65
+ // but we can pass it via 'dispatcher' in undici, or ignore if using global fetch.
66
+ // For simple usage we usually ignore httpsAgent unless strictly needed.
67
+ };
68
+
69
+ const response = await fetch(fullUrl.toString(), fetchOptions);
70
+
71
+ let data: any;
72
+ const contentType = response.headers.get('content-type');
73
+ const contentLength = response.headers.get('content-length');
74
+
75
+ if (response.status === 204 || (contentLength && parseInt(contentLength) === 0)) {
76
+ data = null;
77
+ } else if (contentType && contentType.includes('application/json')) {
78
+ try {
79
+ data = await response.json();
80
+ } catch (error) {
81
+ // If JSON parsing fails (e.g. empty body despite content-type), fallback to text or null
82
+ const text = await response.text();
83
+ try {
84
+ data = JSON.parse(text);
85
+ } catch {
86
+ data = text || null;
87
+ }
88
+ }
89
+ } else {
90
+ data = await response.text();
91
+ }
92
+
93
+ const res: Response<T> = {
94
+ data,
95
+ status: response.status,
96
+ statusText: response.statusText,
97
+ headers: Object.fromEntries(response.headers.entries())
98
+ };
99
+
100
+ const validateStatus = options.validateStatus || this.defaults.validateStatus || ((s) => s >= 200 && s < 300);
101
+ if (!validateStatus(res.status)) {
102
+ const error: any = new Error(`Request failed with status ${res.status}`);
103
+ error.response = res;
104
+ error.status = res.status;
105
+ throw error;
106
+ }
107
+
108
+ return res;
109
+ }
110
+
111
+ async get<T = any>(url: string, config: RequestConfig = {}) {
112
+ return this.request<T>(url, { ...config, method: 'GET' });
113
+ }
114
+
115
+ async post<T = any>(url: string, data?: any, config: RequestConfig = {}) {
116
+ return this.request<T>(url, { ...config, method: 'POST', body: data });
117
+ }
118
+
119
+ async put<T = any>(url: string, data?: any, config: RequestConfig = {}) {
120
+ return this.request<T>(url, { ...config, method: 'PUT', body: data });
121
+ }
122
+
123
+ async delete<T = any>(url: string, config: RequestConfig = {}) {
124
+ return this.request<T>(url, { ...config, method: 'DELETE' });
125
+ }
126
+ }
127
+
128
+ export default HttpClient;
@@ -0,0 +1,87 @@
1
+ export class Spinner {
2
+ private timer: NodeJS.Timeout | null = null;
3
+ private frameIndex = 0;
4
+ private frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
5
+ public text: string;
6
+
7
+ constructor(text: string | { text: string }) {
8
+ this.text = typeof text === 'string' ? text : (text?.text || '');
9
+ }
10
+
11
+ start(text?: string): Spinner {
12
+ if (text) this.text = text;
13
+ this.frameIndex = 0;
14
+ // Only start animation if TTY and not CI (basic check)
15
+ if (process.stdout.isTTY) {
16
+ this.timer = setInterval(() => {
17
+ const frame = this.frames[this.frameIndex];
18
+ this.frameIndex = (this.frameIndex + 1) % this.frames.length;
19
+ this.write(`\r${frame} ${this.text}`);
20
+ }, 80);
21
+ } else {
22
+ console.log(this.text);
23
+ }
24
+ return this;
25
+ }
26
+
27
+ stop(): Spinner {
28
+ if (this.timer) {
29
+ clearInterval(this.timer);
30
+ this.timer = null;
31
+ this.clearLine();
32
+ }
33
+ return this;
34
+ }
35
+
36
+ succeed(text?: string): Spinner {
37
+ this.stop();
38
+ const msg = text || this.text;
39
+ console.log(`${this.color('green', '✔')} ${msg}`);
40
+ return this;
41
+ }
42
+
43
+ fail(text?: string): Spinner {
44
+ this.stop();
45
+ const msg = text || this.text;
46
+ console.log(`${this.color('red', '✖')} ${msg}`);
47
+ return this;
48
+ }
49
+
50
+ info(text?: string): Spinner {
51
+ this.stop();
52
+ console.log(`${this.color('blue', 'ℹ')} ${text || this.text}`);
53
+ return this;
54
+ }
55
+
56
+ warn(text?: string): Spinner {
57
+ this.stop();
58
+ console.log(`${this.color('yellow', '⚠')} ${text || this.text}`);
59
+ return this;
60
+ }
61
+
62
+ private clearLine() {
63
+ if (process.stdout.isTTY) {
64
+ process.stdout.clearLine(0);
65
+ process.stdout.cursorTo(0);
66
+ }
67
+ }
68
+
69
+ private write(str: string) {
70
+ process.stdout.write(str);
71
+ }
72
+
73
+ private color(color: string, str: string): string {
74
+ const colors: any = {
75
+ green: '\x1b[32m',
76
+ red: '\x1b[31m',
77
+ blue: '\x1b[34m',
78
+ yellow: '\x1b[33m',
79
+ reset: '\x1b[0m'
80
+ };
81
+ return `${colors[color] || ''}${str}${colors.reset}`;
82
+ }
83
+ }
84
+
85
+ export default function ora(options: string | { text: string } = '') {
86
+ return new Spinner(options);
87
+ }