@gomjellie/lazyapi 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
@@ -1,16 +1,15 @@
1
1
  # lazyapi
2
2
 
3
- ![demo](https://github.com/user-attachments/assets/ed95f068-09da-4226-91fd-430027258b31)
4
-
5
- **⚠️ Work in Progress**
6
-
7
3
  <div align="center">
8
4
 
9
- A **Vim-style TUI (Terminal User Interface)** tool for exploring Swagger/OpenAPI documents and testing APIs in the terminal
5
+ A **Vim-style TUI (Terminal User Interface)** tool
6
+ for exploring Swagger/OpenAPI documents and testing APIs in the terminal
10
7
 
11
8
  [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT)
12
9
  [![Node Version](https://img.shields.io/badge/node-%3E%3D20.0.0-brightgreen)](https://nodejs.org)
13
10
 
11
+ ![demo](https://github.com/user-attachments/assets/ed95f068-09da-4226-91fd-430027258b31)
12
+
14
13
  </div>
15
14
 
16
15
  ---
@@ -57,7 +56,7 @@ lazyapi openapi.json
57
56
  # YAML file
58
57
  lazyapi swagger.yaml
59
58
 
60
- # URL (coming soon)
59
+ # URL
61
60
  lazyapi https://api.example.com/openapi.json
62
61
  ```
63
62
 
@@ -83,6 +82,7 @@ lazyapi https://api.example.com/openapi.json
83
82
  | Key | Description |
84
83
  | ---------- | ----------------------------------------------------- |
85
84
  | Text input | Enter search query (searches path, description, tags) |
85
+ | `Enter` | Apply search and update results |
86
86
  | `Esc` | Cancel search and return to NORMAL mode |
87
87
 
88
88
  ### Detail View
@@ -125,7 +125,6 @@ lazyapi https://api.example.com/openapi.json
125
125
 
126
126
  ## 📝 Future Plans
127
127
 
128
- - [ ] Load OpenAPI documents from URL
129
128
  - [ ] Multiple environment support (dev, staging, prod)
130
129
  - [ ] Request history storage
131
130
  - [ ] Favorites feature
package/dist/App.js CHANGED
@@ -41,7 +41,7 @@ function AppContent({ filePath }) {
41
41
  // 로딩 상태
42
42
  if (loading) {
43
43
  return (React.createElement(Box, { flexDirection: "column", padding: 1, height: terminalHeight },
44
- React.createElement(LoadingSpinner, { message: "OpenAPI \uBB38\uC11C\uB97C \uB85C\uB529\uD558\uB294 \uC911..." })));
44
+ React.createElement(LoadingSpinner, { message: "Fetching OpenAPI document from URL..." })));
45
45
  }
46
46
  // 에러 상태
47
47
  if (error) {
@@ -4,12 +4,16 @@
4
4
  import React from 'react';
5
5
  import { Box, Text } from 'ink';
6
6
  import Spinner from 'ink-spinner';
7
+ import BigText from 'ink-big-text';
8
+ import Gradient from 'ink-gradient';
7
9
  export default function LoadingSpinner({ message = '로딩 중...' }) {
8
- return (React.createElement(Box, { flexDirection: "column", alignItems: "center", justifyContent: "center", minHeight: 10 },
9
- React.createElement(Box, { marginBottom: 1 },
10
- React.createElement(Text, { color: "green" },
10
+ return (React.createElement(Box, { flexDirection: "column", alignItems: "center", justifyContent: "center", flexGrow: 1 },
11
+ React.createElement(Gradient, { name: "rainbow" },
12
+ React.createElement(BigText, { text: "LAZYAPI" })),
13
+ React.createElement(Box, { marginTop: 1 },
14
+ React.createElement(Text, { color: "cyan" },
11
15
  React.createElement(Spinner, { type: "dots" })),
12
- React.createElement(Text, { color: "green", bold: true },
16
+ React.createElement(Text, { italic: true, color: "gray" },
13
17
  ' ',
14
18
  message))));
15
19
  }
@@ -3,7 +3,9 @@
3
3
  */
4
4
  import React from 'react';
5
5
  import { Box, Text } from 'ink';
6
+ import { useAppSelector } from '../../store/hooks.js';
6
7
  export default function StatusBar({ title, version, currentPath, mode, status }) {
8
+ const navigationCount = useAppSelector((state) => state.app.navigationCount);
7
9
  const getModeColor = () => {
8
10
  switch (mode) {
9
11
  case 'INSERT':
@@ -29,6 +31,7 @@ export default function StatusBar({ title, version, currentPath, mode, status })
29
31
  status && React.createElement(Text, { color: "green" },
30
32
  "\u25CF ",
31
33
  status),
34
+ navigationCount !== null && (React.createElement(Text, { color: "yellow", bold: true }, navigationCount)),
32
35
  React.createElement(Text, { bold: true, color: getModeColor() },
33
36
  "[",
34
37
  mode,
@@ -4,7 +4,8 @@
4
4
  import React from 'react';
5
5
  import { Box, Text, useStdout } from 'ink';
6
6
  import { useAppSelector, useAppDispatch } from '../../store/hooks.js';
7
- import { setMode, detailSetFocus, detailSetPanel, detailSetRequestTab, detailSetResponseTab, detailSetResponse, detailSetParam, } from '../../store/appSlice.js';
7
+ import { setMode, detailSetFocus, detailSetPanel, detailSetRequestTab, detailSetResponseTab, detailSetResponse, detailSetParam, detailSetBody, } from '../../store/appSlice.js';
8
+ import { navigateUp, navigateDown, navigateLeft, navigateRight, } from '../../store/navigationActions.js';
8
9
  import { useNavigation } from '../../hooks/useNavigation.js';
9
10
  import { useKeyPress } from '../../hooks/useKeyPress.js';
10
11
  import { useApiClient } from '../../hooks/useApiClient.js';
@@ -38,7 +39,9 @@ export default function DetailView() {
38
39
  queryParams.length +
39
40
  headerParams.length +
40
41
  bodyParams.length +
41
- formDataParams.length;
42
+ formDataParams.length +
43
+ (endpoint?.requestBody && bodyParams.length === 0 ? 1 : 0) +
44
+ (endpoint?.responses?.length || 0);
42
45
  const handleExecuteRequest = async () => {
43
46
  if (!detail || !document)
44
47
  return;
@@ -60,8 +63,8 @@ export default function DetailView() {
60
63
  if (cmd === 'q' || cmd === 'quit' || cmd === 'exit') {
61
64
  process.exit(0);
62
65
  }
63
- // 2. 접두사 있는 명령어 처리 (pp1, qp1, h1, rb1, rs1, rq1...)
64
- const match = cmd.match(/^(pp|qp|h|rb|rs|rq)(\d+)$/);
66
+ // 2. 접두사 있는 명령어 처리 (pp1, qp1, h1, rqb1, rs1, rq1, rb1...)
67
+ const match = cmd.match(/^(pp|qp|h|rqb|rs|rq|rb)(\d+)$/);
65
68
  if (match) {
66
69
  const type = match[1];
67
70
  const index = parseInt(match[2], 10);
@@ -97,10 +100,9 @@ export default function DetailView() {
97
100
  dispatch(detailSetPanel('request'));
98
101
  }
99
102
  }
100
- else if (type === 'rb') {
101
- // Request Body
103
+ else if (type === 'rqb') {
104
+ // Request Body (기존 rb -> rqb)
102
105
  const bodyStartIndex = pathParams.length + queryParams.length + headerParams.length;
103
- // 만약 bodyParams가 있으면 그쪽으로, 아니면 requestBody(Raw)로
104
106
  if (bodyParams.length > 0) {
105
107
  if (targetIndex >= 0 && targetIndex < bodyParams.length) {
106
108
  dispatch(detailSetFocus(bodyStartIndex + targetIndex));
@@ -108,14 +110,26 @@ export default function DetailView() {
108
110
  }
109
111
  }
110
112
  else if (endpoint?.requestBody) {
111
- // Form Data가 있으면 그 뒤
112
113
  const rawBodyIndex = bodyStartIndex + formDataParams.length;
113
- if (targetIndex === 0) { // rb1 only for raw body
114
+ if (targetIndex === 0) {
114
115
  dispatch(detailSetFocus(rawBodyIndex));
115
116
  dispatch(detailSetPanel('left'));
116
117
  }
117
118
  }
118
119
  }
120
+ else if (type === 'rb') {
121
+ // Responses (사용자 요청: rb)
122
+ const responsesStartIndex = pathParams.length +
123
+ queryParams.length +
124
+ headerParams.length +
125
+ bodyParams.length +
126
+ formDataParams.length +
127
+ (endpoint?.requestBody && bodyParams.length === 0 ? 1 : 0);
128
+ if (targetIndex >= 0 && targetIndex < (endpoint?.responses?.length || 0)) {
129
+ dispatch(detailSetFocus(responsesStartIndex + targetIndex));
130
+ dispatch(detailSetPanel('left'));
131
+ }
132
+ }
119
133
  else if (type === 'rs') {
120
134
  // Response Tabs
121
135
  const tabs = [
@@ -166,28 +180,16 @@ export default function DetailView() {
166
180
  // 키보드 입력 처리 (NORMAL 모드)
167
181
  useKeyPress({
168
182
  j: () => {
169
- if (activePanel === 'left' && totalFields > 0) {
170
- // 다음 필드로
171
- const nextIndex = Math.min(detail.focusedFieldIndex + 1, totalFields - 1);
172
- dispatch(detailSetFocus(nextIndex));
173
- }
183
+ dispatch(navigateDown());
174
184
  },
175
185
  k: () => {
176
- if (activePanel === 'left' && totalFields > 0) {
177
- // 이전 필드로
178
- const prevIndex = Math.max(detail.focusedFieldIndex - 1, 0);
179
- dispatch(detailSetFocus(prevIndex));
180
- }
186
+ dispatch(navigateUp());
181
187
  },
182
188
  h: () => {
183
- // 왼쪽 패널로 (어디서든)
184
- dispatch(detailSetPanel('left'));
189
+ dispatch(navigateLeft());
185
190
  },
186
191
  l: () => {
187
- // 오른쪽(Request) 패널로
188
- if (activePanel === 'left') {
189
- dispatch(detailSetPanel('request'));
190
- }
192
+ dispatch(navigateRight());
191
193
  },
192
194
  onTab: () => {
193
195
  // 패널 순환: Left -> Request -> Response -> Left
@@ -201,12 +203,6 @@ export default function DetailView() {
201
203
  dispatch(detailSetPanel('left'));
202
204
  }
203
205
  },
204
- i: () => {
205
- // INSERT 모드로 전환 (왼쪽 패널에서만)
206
- if (activePanel === 'left') {
207
- dispatch(setMode('INSERT'));
208
- }
209
- },
210
206
  u: () => {
211
207
  // 뒤로 가기
212
208
  goBack();
@@ -224,7 +220,7 @@ export default function DetailView() {
224
220
  onEscape: () => {
225
221
  dispatch(setMode('NORMAL'));
226
222
  dispatch(setCommandInput(''));
227
- }
223
+ },
228
224
  }, mode === 'COMMAND');
229
225
  // INSERT 모드 키보드 입력 처리
230
226
  useKeyPress({
@@ -249,18 +245,22 @@ export default function DetailView() {
249
245
  ];
250
246
  const baseURL = document ? getServerUrl(document) || '' : '';
251
247
  const terminalHeight = stdout?.rows || 24;
252
- // detail 또는 endpoint가 없는 경우 에러 표시
253
- if (!detail || !endpoint) {
248
+ // detail 또는 endpoint 또는 document가 없는 경우 에러 표시
249
+ if (!detail || !endpoint || !document) {
254
250
  return (React.createElement(Box, { flexDirection: "column", height: terminalHeight },
255
251
  React.createElement(StatusBar, { title: "API DETAIL", currentPath: "", mode: mode, status: "ERROR" }),
256
252
  React.createElement(Box, { flexGrow: 1, justifyContent: "center", alignItems: "center" },
257
- React.createElement(Text, { color: "red" }, "\uC0C1\uC138 \uC815\uBCF4\uB97C \uD45C\uC2DC\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.")),
253
+ React.createElement(Text, { color: "red" }, "\uC0C1\uC138 \uC815\uBCF4\uB97C \uD45C\uC2DC\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. (\uBB38\uC11C \uB610\uB294 \uC5D4\uB4DC\uD3EC\uC778\uD2B8 \uC815\uBCF4 \uC5C6\uC74C)")),
258
254
  React.createElement(KeybindingFooter, { bindings: [], mode: mode })));
259
255
  }
260
256
  return (React.createElement(Box, { flexDirection: "column", height: terminalHeight },
261
257
  React.createElement(StatusBar, { title: "API DETAIL", currentPath: endpoint.path, mode: mode, status: loading ? 'LOADING...' : 'READY' }),
262
- React.createElement(Box, { flexGrow: 1, overflow: "hidden" },
263
- React.createElement(LeftPanel, { endpoint: endpoint, parameterValues: detail.parameterValues, focusedFieldIndex: detail.focusedFieldIndex, mode: mode, isFocused: activePanel === 'left', document: document, commandMode: mode === 'COMMAND', onParameterChange: (key, value) => dispatch(detailSetParam({ key, value })), onExitInsertMode: () => dispatch(setMode('NORMAL')) }),
264
- React.createElement(RightPanel, { activeRequestTab: activeRequestTab, activeResponseTab: activeResponseTab, activePanel: activePanel, response: detail.response, endpoint: endpoint, parameterValues: detail.parameterValues, requestBody: detail.requestBody, requestHeaders: detail.headers, baseURL: baseURL, mode: mode, isLoading: loading, commandMode: mode === 'COMMAND' })),
258
+ React.createElement(Box, { flexGrow: 1 },
259
+ React.createElement(LeftPanel, { endpoint: endpoint, parameterValues: detail.parameterValues, requestBodyValue: detail.requestBody, focusedFieldIndex: detail.focusedFieldIndex, mode: mode, isFocused: activePanel === 'left', document: document, commandMode: mode === 'COMMAND', onParameterChange: (key, value) => dispatch(detailSetParam({ key, value })), onExitInsertMode: () => dispatch(setMode('NORMAL')), onRequestBodyEdit: (value) => {
260
+ dispatch(detailSetBody(value));
261
+ dispatch(detailSetPanel('request'));
262
+ dispatch(detailSetRequestTab('body'));
263
+ }, onSetMode: (m) => dispatch(setMode(m)) }),
264
+ React.createElement(RightPanel, { activeRequestTab: activeRequestTab, activeResponseTab: activeResponseTab, activePanel: activePanel, subFocus: detail.subFocus, response: detail.response, endpoint: endpoint, document: document, parameterValues: detail.parameterValues, requestBody: detail.requestBody, requestHeaders: detail.headers, baseURL: baseURL, mode: mode, isLoading: loading, commandMode: mode === 'COMMAND' })),
265
265
  React.createElement(KeybindingFooter, { bindings: keyBindings, mode: mode, commandInput: commandInput, onCommandChange: (value) => dispatch(setCommandInput(value)), onCommandSubmit: handleCommandSubmit })));
266
266
  }
@@ -7,6 +7,7 @@ import { AppMode } from '../../types/app-state.js';
7
7
  interface LeftPanelProps {
8
8
  endpoint: Endpoint;
9
9
  parameterValues: Record<string, string>;
10
+ requestBodyValue?: string;
10
11
  focusedFieldIndex: number;
11
12
  mode: AppMode;
12
13
  isFocused: boolean;
@@ -14,6 +15,8 @@ interface LeftPanelProps {
14
15
  commandMode?: boolean;
15
16
  onParameterChange: (key: string, value: string) => void;
16
17
  onExitInsertMode?: () => void;
18
+ onRequestBodyEdit?: (value: string) => void;
19
+ onSetMode?: (mode: AppMode) => void;
17
20
  }
18
- export default function LeftPanel({ endpoint, parameterValues, focusedFieldIndex, mode, isFocused, document, commandMode, onParameterChange, onExitInsertMode, }: LeftPanelProps): React.JSX.Element;
21
+ export default function LeftPanel({ endpoint, parameterValues, requestBodyValue, focusedFieldIndex, mode, isFocused, document, commandMode, onParameterChange, onExitInsertMode, onRequestBodyEdit, onSetMode, }: LeftPanelProps): React.JSX.Element;
19
22
  export {};