@decencia/ch-cli 1.0.0 → 1.1.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.
- package/dist/commands/setup-skill.d.ts +3 -0
- package/dist/commands/setup-skill.d.ts.map +1 -0
- package/dist/commands/setup-skill.js +65 -0
- package/dist/commands/setup-skill.js.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/package.json +5 -1
- package/skill/SKILL.md +490 -0
- package/src/client.ts +0 -137
- package/src/commands/archives.ts +0 -334
- package/src/commands/auth.ts +0 -154
- package/src/commands/dashboard.ts +0 -112
- package/src/commands/db-schema.ts +0 -74
- package/src/commands/dev-status.ts +0 -166
- package/src/commands/init.ts +0 -92
- package/src/commands/members.ts +0 -92
- package/src/commands/notifications.ts +0 -169
- package/src/commands/prd.ts +0 -72
- package/src/commands/projects.ts +0 -207
- package/src/commands/qna.ts +0 -205
- package/src/commands/specs.ts +0 -257
- package/src/commands/sprints.ts +0 -255
- package/src/commands/sqa.ts +0 -422
- package/src/config.ts +0 -98
- package/src/index.ts +0 -69
- package/src/output.ts +0 -186
- package/src/utils.ts +0 -3
- package/tsconfig.json +0 -18
package/src/client.ts
DELETED
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
import axios, { AxiosInstance, AxiosError } from 'axios';
|
|
2
|
-
import { loadConfig, getProjectContext, ProjectContext } from './config';
|
|
3
|
-
import { printError } from './output';
|
|
4
|
-
|
|
5
|
-
export interface ClientOptions {
|
|
6
|
-
apiKey?: string;
|
|
7
|
-
projectId?: string;
|
|
8
|
-
apiUrl?: string;
|
|
9
|
-
verbose?: boolean;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
let _lastProjectContext: ProjectContext | null = null;
|
|
13
|
-
|
|
14
|
-
export function getLastProjectContext(): ProjectContext | null {
|
|
15
|
-
return _lastProjectContext;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function createClient(options: ClientOptions = {}): AxiosInstance {
|
|
19
|
-
const config = loadConfig();
|
|
20
|
-
|
|
21
|
-
const apiKey = options.apiKey || config?.apiKey;
|
|
22
|
-
const projectCtx = getProjectContext(options.projectId);
|
|
23
|
-
_lastProjectContext = projectCtx;
|
|
24
|
-
const projectId = projectCtx?.projectId;
|
|
25
|
-
const apiUrl = options.apiUrl || config?.apiUrl || 'https://ch-api-618529407342.asia-northeast3.run.app';
|
|
26
|
-
|
|
27
|
-
if (!apiKey) {
|
|
28
|
-
printError('API Key가 설정되지 않았습니다. "ch auth login" 명령어로 로그인하세요.');
|
|
29
|
-
process.exit(1);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const instance = axios.create({
|
|
33
|
-
baseURL: apiUrl,
|
|
34
|
-
timeout: 30000,
|
|
35
|
-
headers: {
|
|
36
|
-
'Content-Type': 'application/json',
|
|
37
|
-
},
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
// Request interceptor: inject auth headers
|
|
41
|
-
instance.interceptors.request.use((req) => {
|
|
42
|
-
req.headers['Authorization'] = `Bearer ${apiKey}`;
|
|
43
|
-
if (projectId) {
|
|
44
|
-
req.headers['X-Project-Id'] = projectId;
|
|
45
|
-
}
|
|
46
|
-
if (options.verbose) {
|
|
47
|
-
const method = (req.method || 'GET').toUpperCase();
|
|
48
|
-
const url = `${req.baseURL}${req.url}`;
|
|
49
|
-
console.error(`[DEBUG] ${method} ${url}`);
|
|
50
|
-
if (req.params && Object.keys(req.params).length > 0) {
|
|
51
|
-
console.error(`[DEBUG] Params: ${JSON.stringify(req.params)}`);
|
|
52
|
-
}
|
|
53
|
-
if (req.data) {
|
|
54
|
-
console.error(`[DEBUG] Body: ${JSON.stringify(req.data)}`);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
return req;
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
// Response error interceptor
|
|
61
|
-
instance.interceptors.response.use(
|
|
62
|
-
(response) => response,
|
|
63
|
-
(error: AxiosError<{ message?: string; error?: string }>) => {
|
|
64
|
-
if (!error.response) {
|
|
65
|
-
printError('서버에 연결할 수 없습니다. API URL과 네트워크 상태를 확인하세요.');
|
|
66
|
-
process.exit(1);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const status = error.response.status;
|
|
70
|
-
const data = error.response.data;
|
|
71
|
-
const serverMessage = data?.message || data?.error || '';
|
|
72
|
-
|
|
73
|
-
switch (status) {
|
|
74
|
-
case 400:
|
|
75
|
-
printError(`잘못된 요청입니다: ${serverMessage}`);
|
|
76
|
-
break;
|
|
77
|
-
case 401:
|
|
78
|
-
printError(`인증 실패: API Key가 유효하지 않거나 만료되었습니다. ${serverMessage}`);
|
|
79
|
-
break;
|
|
80
|
-
case 403:
|
|
81
|
-
printError(`권한이 없습니다: ${serverMessage}`);
|
|
82
|
-
break;
|
|
83
|
-
case 404:
|
|
84
|
-
printError(`리소스를 찾을 수 없습니다: ${serverMessage}`);
|
|
85
|
-
break;
|
|
86
|
-
case 409:
|
|
87
|
-
printError(`충돌: ${serverMessage}`);
|
|
88
|
-
break;
|
|
89
|
-
case 410:
|
|
90
|
-
printError(`만료됨: ${serverMessage}`);
|
|
91
|
-
break;
|
|
92
|
-
case 500:
|
|
93
|
-
printError(`서버 내부 오류: ${serverMessage}`);
|
|
94
|
-
break;
|
|
95
|
-
default:
|
|
96
|
-
printError(`오류 (${status}): ${serverMessage}`);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
return Promise.reject(error);
|
|
100
|
-
}
|
|
101
|
-
);
|
|
102
|
-
|
|
103
|
-
return instance;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Create a client that doesn't require projectId (for invitation endpoints)
|
|
108
|
-
*/
|
|
109
|
-
export function createUnauthClient(options: ClientOptions = {}): AxiosInstance {
|
|
110
|
-
const config = loadConfig();
|
|
111
|
-
const apiUrl = options.apiUrl || config?.apiUrl || 'https://ch-api-618529407342.asia-northeast3.run.app';
|
|
112
|
-
|
|
113
|
-
const instance = axios.create({
|
|
114
|
-
baseURL: apiUrl,
|
|
115
|
-
timeout: 30000,
|
|
116
|
-
headers: {
|
|
117
|
-
'Content-Type': 'application/json',
|
|
118
|
-
},
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
instance.interceptors.response.use(
|
|
122
|
-
(response) => response,
|
|
123
|
-
(error: AxiosError<{ message?: string; error?: string }>) => {
|
|
124
|
-
if (!error.response) {
|
|
125
|
-
printError('서버에 연결할 수 없습니다.');
|
|
126
|
-
process.exit(1);
|
|
127
|
-
}
|
|
128
|
-
const status = error.response.status;
|
|
129
|
-
const data = error.response.data;
|
|
130
|
-
const serverMessage = data?.message || data?.error || '';
|
|
131
|
-
printError(`오류 (${status}): ${serverMessage}`);
|
|
132
|
-
return Promise.reject(error);
|
|
133
|
-
}
|
|
134
|
-
);
|
|
135
|
-
|
|
136
|
-
return instance;
|
|
137
|
-
}
|
package/src/commands/archives.ts
DELETED
|
@@ -1,334 +0,0 @@
|
|
|
1
|
-
import { Command } from 'commander';
|
|
2
|
-
import * as fs from 'fs';
|
|
3
|
-
import * as path from 'path';
|
|
4
|
-
import FormData from 'form-data';
|
|
5
|
-
import { createClient } from '../client';
|
|
6
|
-
import { formatOutput, printSuccess, printError, printInfo } from '../output';
|
|
7
|
-
|
|
8
|
-
export function registerArchivesCommand(program: Command): void {
|
|
9
|
-
const archives = program
|
|
10
|
-
.command('archives')
|
|
11
|
-
.description('아카이브 관리');
|
|
12
|
-
|
|
13
|
-
// ch archives list
|
|
14
|
-
archives
|
|
15
|
-
.command('list')
|
|
16
|
-
.description('아카이브 목록 조회')
|
|
17
|
-
.option('--category <category>', '카테고리 필터')
|
|
18
|
-
.option('--date-from <date>', '문서일자 시작')
|
|
19
|
-
.option('--date-to <date>', '문서일자 끝')
|
|
20
|
-
.option('--search <keyword>', '제목/메모 검색')
|
|
21
|
-
.option('--sort <field>', '정렬 필드')
|
|
22
|
-
.option('--desc', '내림차순 정렬')
|
|
23
|
-
.option('--page <number>', '페이지 번호', '1')
|
|
24
|
-
.action(async (options) => {
|
|
25
|
-
const globalOpts = program.opts();
|
|
26
|
-
try {
|
|
27
|
-
const client = createClient({ projectId: globalOpts.project });
|
|
28
|
-
const params: any = {};
|
|
29
|
-
if (options.category) params.category = options.category;
|
|
30
|
-
if (options.dateFrom) params.dateFrom = options.dateFrom;
|
|
31
|
-
if (options.dateTo) params.dateTo = options.dateTo;
|
|
32
|
-
if (options.search) params.search = options.search;
|
|
33
|
-
if (options.sort) params.sortBy = options.sort;
|
|
34
|
-
if (options.desc) params.sortOrder = 'desc';
|
|
35
|
-
params.page = parseInt(options.page, 10);
|
|
36
|
-
|
|
37
|
-
const { data } = await client.get('/archives', { params });
|
|
38
|
-
formatOutput(data, globalOpts);
|
|
39
|
-
} catch (error) {
|
|
40
|
-
// Error already handled
|
|
41
|
-
}
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
// ch archives get
|
|
45
|
-
archives
|
|
46
|
-
.command('get <archiveId>')
|
|
47
|
-
.description('아카이브 상세 조회')
|
|
48
|
-
.action(async (archiveId: string) => {
|
|
49
|
-
const globalOpts = program.opts();
|
|
50
|
-
try {
|
|
51
|
-
const client = createClient({ projectId: globalOpts.project });
|
|
52
|
-
const { data } = await client.get(`/archives/${archiveId}`);
|
|
53
|
-
formatOutput(data, globalOpts);
|
|
54
|
-
} catch (error) {
|
|
55
|
-
// Error already handled
|
|
56
|
-
}
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
// ch archives create
|
|
60
|
-
archives
|
|
61
|
-
.command('create')
|
|
62
|
-
.description('아카이브 등록')
|
|
63
|
-
.requiredOption('--title <title>', '제목')
|
|
64
|
-
.requiredOption('--category <category>', '카테고리')
|
|
65
|
-
.requiredOption('--date <date>', '문서일자 (YYYY-MM-DD)')
|
|
66
|
-
.option('--files <filePaths>', '첨부파일 경로 (쉼표 구분)')
|
|
67
|
-
.option('--memo <memo>', '메모')
|
|
68
|
-
.action(async (options) => {
|
|
69
|
-
const globalOpts = program.opts();
|
|
70
|
-
try {
|
|
71
|
-
if (globalOpts.dryRun) {
|
|
72
|
-
printInfo(`[DRY-RUN] 아카이브 "${options.title}" 등록 요청`);
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const form = new FormData();
|
|
77
|
-
form.append('title', options.title);
|
|
78
|
-
form.append('category', options.category);
|
|
79
|
-
form.append('documentDate', options.date);
|
|
80
|
-
if (options.memo) form.append('memo', options.memo);
|
|
81
|
-
|
|
82
|
-
if (options.files) {
|
|
83
|
-
const filePaths = options.files.split(',').map((f: string) => f.trim());
|
|
84
|
-
for (const filePath of filePaths) {
|
|
85
|
-
const resolvedPath = path.resolve(filePath);
|
|
86
|
-
if (!fs.existsSync(resolvedPath)) {
|
|
87
|
-
printError(`파일을 찾을 수 없습니다: ${resolvedPath}`);
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
form.append('files', fs.createReadStream(resolvedPath));
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const client = createClient({ projectId: globalOpts.project });
|
|
95
|
-
const { data } = await client.post('/archives', form, {
|
|
96
|
-
headers: {
|
|
97
|
-
...form.getHeaders(),
|
|
98
|
-
},
|
|
99
|
-
});
|
|
100
|
-
formatOutput(data, globalOpts);
|
|
101
|
-
printSuccess('아카이브가 등록되었습니다.');
|
|
102
|
-
} catch (error) {
|
|
103
|
-
// Error already handled
|
|
104
|
-
}
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
// ch archives update
|
|
108
|
-
archives
|
|
109
|
-
.command('update <archiveId>')
|
|
110
|
-
.description('아카이브 수정')
|
|
111
|
-
.option('--title <title>', '제목')
|
|
112
|
-
.option('--category <category>', '카테고리')
|
|
113
|
-
.option('--date <date>', '문서일자')
|
|
114
|
-
.option('--memo <memo>', '메모')
|
|
115
|
-
.action(async (archiveId: string, options) => {
|
|
116
|
-
const globalOpts = program.opts();
|
|
117
|
-
try {
|
|
118
|
-
if (globalOpts.dryRun) {
|
|
119
|
-
printInfo(`[DRY-RUN] 아카이브 "${archiveId}" 수정 요청`);
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
122
|
-
const body: any = {};
|
|
123
|
-
if (options.title) body.title = options.title;
|
|
124
|
-
if (options.category) body.category = options.category;
|
|
125
|
-
if (options.date) body.documentDate = options.date;
|
|
126
|
-
if (options.memo) body.memo = options.memo;
|
|
127
|
-
|
|
128
|
-
if (Object.keys(body).length === 0) {
|
|
129
|
-
printError('수정할 필드를 지정하세요.');
|
|
130
|
-
return;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
const client = createClient({ projectId: globalOpts.project });
|
|
134
|
-
const { data } = await client.patch(`/archives/${archiveId}`, body);
|
|
135
|
-
formatOutput(data, globalOpts);
|
|
136
|
-
printSuccess('아카이브가 수정되었습니다.');
|
|
137
|
-
} catch (error) {
|
|
138
|
-
// Error already handled
|
|
139
|
-
}
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
// ch archives add-files
|
|
143
|
-
archives
|
|
144
|
-
.command('add-files <archiveId>')
|
|
145
|
-
.description('아카이브에 파일 추가')
|
|
146
|
-
.requiredOption('--files <filePaths>', '추가할 파일 경로 (쉼표 구분)')
|
|
147
|
-
.action(async (archiveId: string, options) => {
|
|
148
|
-
const globalOpts = program.opts();
|
|
149
|
-
try {
|
|
150
|
-
if (globalOpts.dryRun) {
|
|
151
|
-
printInfo(`[DRY-RUN] 아카이브 "${archiveId}"에 파일 추가 요청`);
|
|
152
|
-
return;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
const form = new FormData();
|
|
156
|
-
const filePaths = options.files.split(',').map((f: string) => f.trim());
|
|
157
|
-
for (const filePath of filePaths) {
|
|
158
|
-
const resolvedPath = path.resolve(filePath);
|
|
159
|
-
if (!fs.existsSync(resolvedPath)) {
|
|
160
|
-
printError(`파일을 찾을 수 없습니다: ${resolvedPath}`);
|
|
161
|
-
return;
|
|
162
|
-
}
|
|
163
|
-
form.append('files', fs.createReadStream(resolvedPath));
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
const client = createClient({ projectId: globalOpts.project });
|
|
167
|
-
const { data } = await client.post(`/archives/${archiveId}/files`, form, {
|
|
168
|
-
headers: {
|
|
169
|
-
...form.getHeaders(),
|
|
170
|
-
},
|
|
171
|
-
});
|
|
172
|
-
formatOutput(data, globalOpts);
|
|
173
|
-
printSuccess('파일이 추가되었습니다.');
|
|
174
|
-
} catch (error) {
|
|
175
|
-
// Error already handled
|
|
176
|
-
}
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
// ch archives remove-file
|
|
180
|
-
archives
|
|
181
|
-
.command('remove-file <archiveId> <fileName>')
|
|
182
|
-
.description('아카이브 첨부파일 삭제')
|
|
183
|
-
.action(async (archiveId: string, fileName: string) => {
|
|
184
|
-
const globalOpts = program.opts();
|
|
185
|
-
try {
|
|
186
|
-
if (globalOpts.dryRun) {
|
|
187
|
-
printInfo(`[DRY-RUN] 아카이브 "${archiveId}" 파일 "${fileName}" 삭제 요청`);
|
|
188
|
-
return;
|
|
189
|
-
}
|
|
190
|
-
const client = createClient({ projectId: globalOpts.project });
|
|
191
|
-
await client.delete(`/archives/${archiveId}/files/${encodeURIComponent(fileName)}`);
|
|
192
|
-
printSuccess(`파일 "${fileName}"이 삭제되었습니다.`);
|
|
193
|
-
} catch (error) {
|
|
194
|
-
// Error already handled
|
|
195
|
-
}
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
// ch archives delete
|
|
199
|
-
archives
|
|
200
|
-
.command('delete <archiveId>')
|
|
201
|
-
.description('아카이브 삭제')
|
|
202
|
-
.action(async (archiveId: string) => {
|
|
203
|
-
const globalOpts = program.opts();
|
|
204
|
-
try {
|
|
205
|
-
if (globalOpts.dryRun) {
|
|
206
|
-
printInfo(`[DRY-RUN] 아카이브 "${archiveId}" 삭제 요청`);
|
|
207
|
-
return;
|
|
208
|
-
}
|
|
209
|
-
const client = createClient({ projectId: globalOpts.project });
|
|
210
|
-
await client.delete(`/archives/${archiveId}`);
|
|
211
|
-
printSuccess('아카이브가 삭제되었습니다.');
|
|
212
|
-
} catch (error) {
|
|
213
|
-
// Error already handled
|
|
214
|
-
}
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
// ch archives download
|
|
218
|
-
archives
|
|
219
|
-
.command('download <archiveId>')
|
|
220
|
-
.description('아카이브 파일 다운로드')
|
|
221
|
-
.option('--file <fileName>', '특정 파일명 (미지정 시 전체 zip)')
|
|
222
|
-
.requiredOption('--output <dir>', '저장 디렉토리')
|
|
223
|
-
.action(async (archiveId: string, options) => {
|
|
224
|
-
const globalOpts = program.opts();
|
|
225
|
-
try {
|
|
226
|
-
const client = createClient({ projectId: globalOpts.project });
|
|
227
|
-
const params: any = {};
|
|
228
|
-
if (options.file) params.file = options.file;
|
|
229
|
-
|
|
230
|
-
const response = await client.get(`/archives/${archiveId}/download`, {
|
|
231
|
-
params,
|
|
232
|
-
responseType: 'arraybuffer',
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
// Determine file name from Content-Disposition header or use default
|
|
236
|
-
const contentDisposition = response.headers['content-disposition'];
|
|
237
|
-
let fileName = options.file || `archive-${archiveId}.zip`;
|
|
238
|
-
if (contentDisposition) {
|
|
239
|
-
const match = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/);
|
|
240
|
-
if (match && match[1]) {
|
|
241
|
-
fileName = match[1].replace(/['"]/g, '');
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
const outputDir = path.resolve(options.output);
|
|
246
|
-
if (!fs.existsSync(outputDir)) {
|
|
247
|
-
fs.mkdirSync(outputDir, { recursive: true });
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
const outputPath = path.join(outputDir, fileName);
|
|
251
|
-
fs.writeFileSync(outputPath, Buffer.from(response.data));
|
|
252
|
-
printSuccess(`파일이 저장되었습니다: ${outputPath}`);
|
|
253
|
-
} catch (error) {
|
|
254
|
-
// Error already handled
|
|
255
|
-
}
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
// ch archives navigate
|
|
259
|
-
archives
|
|
260
|
-
.command('navigate <archiveId>')
|
|
261
|
-
.description('이전/다음 아카이브 조회')
|
|
262
|
-
.requiredOption('--direction <direction>', '방향 (prev|next)')
|
|
263
|
-
.action(async (archiveId: string, options) => {
|
|
264
|
-
const globalOpts = program.opts();
|
|
265
|
-
try {
|
|
266
|
-
const client = createClient({ projectId: globalOpts.project });
|
|
267
|
-
const { data } = await client.get(`/archives/${archiveId}/navigate`, {
|
|
268
|
-
params: { direction: options.direction },
|
|
269
|
-
});
|
|
270
|
-
formatOutput(data, globalOpts);
|
|
271
|
-
} catch (error) {
|
|
272
|
-
// Error already handled
|
|
273
|
-
}
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
// ch archives categories
|
|
277
|
-
const categories = archives
|
|
278
|
-
.command('categories')
|
|
279
|
-
.description('아카이브 카테고리 관리');
|
|
280
|
-
|
|
281
|
-
// ch archives categories list
|
|
282
|
-
categories
|
|
283
|
-
.command('list')
|
|
284
|
-
.description('카테고리 목록 조회')
|
|
285
|
-
.action(async () => {
|
|
286
|
-
const globalOpts = program.opts();
|
|
287
|
-
try {
|
|
288
|
-
const client = createClient({ projectId: globalOpts.project });
|
|
289
|
-
const { data } = await client.get('/archives/categories');
|
|
290
|
-
formatOutput(data, globalOpts);
|
|
291
|
-
} catch (error) {
|
|
292
|
-
// Error already handled
|
|
293
|
-
}
|
|
294
|
-
});
|
|
295
|
-
|
|
296
|
-
// ch archives categories add
|
|
297
|
-
categories
|
|
298
|
-
.command('add <name>')
|
|
299
|
-
.description('카테고리 추가')
|
|
300
|
-
.action(async (name: string) => {
|
|
301
|
-
const globalOpts = program.opts();
|
|
302
|
-
try {
|
|
303
|
-
if (globalOpts.dryRun) {
|
|
304
|
-
printInfo(`[DRY-RUN] 카테고리 "${name}" 추가 요청`);
|
|
305
|
-
return;
|
|
306
|
-
}
|
|
307
|
-
const client = createClient({ projectId: globalOpts.project });
|
|
308
|
-
const { data } = await client.post('/archives/categories', { name });
|
|
309
|
-
formatOutput(data, globalOpts);
|
|
310
|
-
printSuccess(`카테고리 "${name}"이 추가되었습니다.`);
|
|
311
|
-
} catch (error) {
|
|
312
|
-
// Error already handled
|
|
313
|
-
}
|
|
314
|
-
});
|
|
315
|
-
|
|
316
|
-
// ch archives categories remove
|
|
317
|
-
categories
|
|
318
|
-
.command('remove <name>')
|
|
319
|
-
.description('카테고리 삭제')
|
|
320
|
-
.action(async (name: string) => {
|
|
321
|
-
const globalOpts = program.opts();
|
|
322
|
-
try {
|
|
323
|
-
if (globalOpts.dryRun) {
|
|
324
|
-
printInfo(`[DRY-RUN] 카테고리 "${name}" 삭제 요청`);
|
|
325
|
-
return;
|
|
326
|
-
}
|
|
327
|
-
const client = createClient({ projectId: globalOpts.project });
|
|
328
|
-
await client.delete(`/archives/categories/${encodeURIComponent(name)}`);
|
|
329
|
-
printSuccess(`카테고리 "${name}"이 삭제되었습니다.`);
|
|
330
|
-
} catch (error) {
|
|
331
|
-
// Error already handled
|
|
332
|
-
}
|
|
333
|
-
});
|
|
334
|
-
}
|
package/src/commands/auth.ts
DELETED
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
import { Command } from 'commander';
|
|
2
|
-
import { loadConfig, saveConfig, clearConfig, getConfigPath } from '../config';
|
|
3
|
-
import { createClient } from '../client';
|
|
4
|
-
import { formatOutput, printSuccess, printError, printInfo } from '../output';
|
|
5
|
-
|
|
6
|
-
export function registerAuthCommand(program: Command): void {
|
|
7
|
-
const auth = program
|
|
8
|
-
.command('auth')
|
|
9
|
-
.description('인증 및 세션 관리');
|
|
10
|
-
|
|
11
|
-
// ch auth login
|
|
12
|
-
auth
|
|
13
|
-
.command('login')
|
|
14
|
-
.description('API Key와 프로젝트 ID로 로그인')
|
|
15
|
-
.requiredOption('--key <key>', 'API Key')
|
|
16
|
-
.requiredOption('--project-id <projectId>', '프로젝트 ID')
|
|
17
|
-
.option('--api-url <url>', 'API 서버 URL', 'https://ch-api-618529407342.asia-northeast3.run.app')
|
|
18
|
-
.action(async (options) => {
|
|
19
|
-
try {
|
|
20
|
-
const config = {
|
|
21
|
-
apiKey: options.key,
|
|
22
|
-
projectId: options.projectId,
|
|
23
|
-
apiUrl: options.apiUrl,
|
|
24
|
-
defaultFormat: 'table' as const,
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
// Verify credentials by calling auth status
|
|
28
|
-
const client = createClient({
|
|
29
|
-
apiKey: config.apiKey,
|
|
30
|
-
projectId: config.projectId,
|
|
31
|
-
apiUrl: config.apiUrl,
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
try {
|
|
35
|
-
await client.get('/auth/status');
|
|
36
|
-
} catch {
|
|
37
|
-
printError('인증 실패: API Key 또는 프로젝트 ID를 확인하세요.');
|
|
38
|
-
process.exit(1);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
saveConfig(config);
|
|
42
|
-
printSuccess(`로그인 성공! 설정이 ${getConfigPath()}에 저장되었습니다.`);
|
|
43
|
-
} catch (error) {
|
|
44
|
-
// Error already handled by interceptor
|
|
45
|
-
}
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
// ch auth status
|
|
49
|
-
auth
|
|
50
|
-
.command('status')
|
|
51
|
-
.description('현재 인증 상태 확인')
|
|
52
|
-
.action(async () => {
|
|
53
|
-
const globalOpts = program.opts();
|
|
54
|
-
try {
|
|
55
|
-
const config = loadConfig();
|
|
56
|
-
if (!config) {
|
|
57
|
-
printError('로그인되어 있지 않습니다. "ch auth login" 명령어로 로그인하세요.');
|
|
58
|
-
process.exit(1);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const client = createClient();
|
|
62
|
-
const { data } = await client.get('/auth/status');
|
|
63
|
-
formatOutput(data, globalOpts);
|
|
64
|
-
} catch (error) {
|
|
65
|
-
// Error already handled by interceptor
|
|
66
|
-
}
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
// ch auth logout
|
|
70
|
-
auth
|
|
71
|
-
.command('logout')
|
|
72
|
-
.description('로그아웃 (로컬 설정 삭제)')
|
|
73
|
-
.action(() => {
|
|
74
|
-
clearConfig();
|
|
75
|
-
printSuccess('로그아웃되었습니다.');
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
// ch auth keys
|
|
79
|
-
const keys = auth
|
|
80
|
-
.command('keys')
|
|
81
|
-
.description('API Key 관리');
|
|
82
|
-
|
|
83
|
-
// ch auth keys list
|
|
84
|
-
keys
|
|
85
|
-
.command('list')
|
|
86
|
-
.description('내 API Key 목록 조회')
|
|
87
|
-
.action(async () => {
|
|
88
|
-
const globalOpts = program.opts();
|
|
89
|
-
try {
|
|
90
|
-
const client = createClient();
|
|
91
|
-
const { data } = await client.get('/auth/keys');
|
|
92
|
-
formatOutput(data, globalOpts);
|
|
93
|
-
} catch (error) {
|
|
94
|
-
// Error already handled by interceptor
|
|
95
|
-
}
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
// ch auth keys create
|
|
99
|
-
keys
|
|
100
|
-
.command('create')
|
|
101
|
-
.description('새 API Key 발급')
|
|
102
|
-
.requiredOption('--name <name>', 'API Key 이름')
|
|
103
|
-
.action(async (options) => {
|
|
104
|
-
const globalOpts = program.opts();
|
|
105
|
-
try {
|
|
106
|
-
if (globalOpts.dryRun) {
|
|
107
|
-
printInfo(`[DRY-RUN] API Key "${options.name}" 생성 요청`);
|
|
108
|
-
return;
|
|
109
|
-
}
|
|
110
|
-
const client = createClient();
|
|
111
|
-
const { data } = await client.post('/auth/keys', { name: options.name });
|
|
112
|
-
formatOutput(data, globalOpts);
|
|
113
|
-
printSuccess('API Key가 발급되었습니다. 이 키는 다시 표시되지 않으니 안전하게 보관하세요.');
|
|
114
|
-
} catch (error) {
|
|
115
|
-
// Error already handled by interceptor
|
|
116
|
-
}
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
// ch auth keys revoke
|
|
120
|
-
keys
|
|
121
|
-
.command('revoke <keyId>')
|
|
122
|
-
.description('API Key 폐기')
|
|
123
|
-
.action(async (keyId: string) => {
|
|
124
|
-
const globalOpts = program.opts();
|
|
125
|
-
try {
|
|
126
|
-
if (globalOpts.dryRun) {
|
|
127
|
-
printInfo(`[DRY-RUN] API Key "${keyId}" 폐기 요청`);
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
130
|
-
const client = createClient();
|
|
131
|
-
await client.delete(`/auth/keys/${keyId}`);
|
|
132
|
-
printSuccess(`API Key "${keyId}"가 폐기되었습니다.`);
|
|
133
|
-
} catch (error) {
|
|
134
|
-
// Error already handled by interceptor
|
|
135
|
-
}
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
// ch auth switch
|
|
139
|
-
auth
|
|
140
|
-
.command('switch')
|
|
141
|
-
.description('프로젝트 전환')
|
|
142
|
-
.requiredOption('--project-id <projectId>', '전환할 프로젝트 ID')
|
|
143
|
-
.action(async (options) => {
|
|
144
|
-
const config = loadConfig();
|
|
145
|
-
if (!config) {
|
|
146
|
-
printError('로그인되어 있지 않습니다. "ch auth login" 명령어로 로그인하세요.');
|
|
147
|
-
process.exit(1);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
config.projectId = options.projectId;
|
|
151
|
-
saveConfig(config);
|
|
152
|
-
printSuccess(`프로젝트가 "${options.projectId}"로 전환되었습니다.`);
|
|
153
|
-
});
|
|
154
|
-
}
|