@decencia/ch-cli 1.0.0 → 1.2.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,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
- }
@@ -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
- }
@@ -1,112 +0,0 @@
1
- import { Command } from 'commander';
2
- import { createClient } from '../client';
3
- import { formatOutput } from '../output';
4
-
5
- export function registerDashboardCommand(program: Command): void {
6
- const dashboard = program
7
- .command('dashboard')
8
- .description('대시보드')
9
- .action(async () => {
10
- const globalOpts = program.opts();
11
- try {
12
- const client = createClient({ projectId: globalOpts.project });
13
- const { data } = await client.get('/dashboard');
14
- formatOutput(data, globalOpts);
15
- } catch (error) {
16
- // Error already handled
17
- }
18
- });
19
-
20
- // ch dashboard progress
21
- dashboard
22
- .command('progress')
23
- .description('전체 진행률 조회')
24
- .action(async () => {
25
- const globalOpts = program.opts();
26
- try {
27
- const client = createClient({ projectId: globalOpts.project });
28
- const { data } = await client.get('/dashboard/progress');
29
- formatOutput(data, globalOpts);
30
- } catch (error) {
31
- // Error already handled
32
- }
33
- });
34
-
35
- // ch dashboard sprint
36
- dashboard
37
- .command('sprint')
38
- .description('현재 Sprint 요약')
39
- .action(async () => {
40
- const globalOpts = program.opts();
41
- try {
42
- const client = createClient({ projectId: globalOpts.project });
43
- const { data } = await client.get('/dashboard/sprint');
44
- formatOutput(data, globalOpts);
45
- } catch (error) {
46
- // Error already handled
47
- }
48
- });
49
-
50
- // ch dashboard qna-pending
51
- dashboard
52
- .command('qna-pending')
53
- .description('답변대기 QnA 조회')
54
- .action(async () => {
55
- const globalOpts = program.opts();
56
- try {
57
- const client = createClient({ projectId: globalOpts.project });
58
- const { data } = await client.get('/dashboard/qna-pending');
59
- formatOutput(data, globalOpts);
60
- } catch (error) {
61
- // Error already handled
62
- }
63
- });
64
-
65
- // ch dashboard sqa-latest
66
- dashboard
67
- .command('sqa-latest')
68
- .description('최근 SQA 결과 조회')
69
- .action(async () => {
70
- const globalOpts = program.opts();
71
- try {
72
- const client = createClient({ projectId: globalOpts.project });
73
- const { data } = await client.get('/dashboard/sqa-latest');
74
- formatOutput(data, globalOpts);
75
- } catch (error) {
76
- // Error already handled
77
- }
78
- });
79
-
80
- // ch dashboard recent-changes
81
- dashboard
82
- .command('recent-changes')
83
- .description('최근 변경 이력 조회')
84
- .option('--limit <number>', '조회 건수', '10')
85
- .action(async (options) => {
86
- const globalOpts = program.opts();
87
- try {
88
- const client = createClient({ projectId: globalOpts.project });
89
- const { data } = await client.get('/dashboard/recent-changes', {
90
- params: { limit: parseInt(options.limit, 10) },
91
- });
92
- formatOutput(data, globalOpts);
93
- } catch (error) {
94
- // Error already handled
95
- }
96
- });
97
-
98
- // ch dashboard members-summary
99
- dashboard
100
- .command('members-summary')
101
- .description('멤버 요약 조회')
102
- .action(async () => {
103
- const globalOpts = program.opts();
104
- try {
105
- const client = createClient({ projectId: globalOpts.project });
106
- const { data } = await client.get('/dashboard/members-summary');
107
- formatOutput(data, globalOpts);
108
- } catch (error) {
109
- // Error already handled
110
- }
111
- });
112
- }
@@ -1,74 +0,0 @@
1
- import { Command } from 'commander';
2
- import { createClient } from '../client';
3
- import { formatOutput, printSuccess, printError, printInfo } from '../output';
4
-
5
- export function registerDbSchemaCommand(program: Command): void {
6
- const dbSchema = program
7
- .command('db-schema')
8
- .description('DB 스키마 관리');
9
-
10
- // ch db-schema get
11
- dbSchema
12
- .command('get')
13
- .description('DB 스키마 조회')
14
- .action(async () => {
15
- const globalOpts = program.opts();
16
- try {
17
- const client = createClient({ projectId: globalOpts.project });
18
- const { data } = await client.get('/db-schema');
19
- if (!data) {
20
- printInfo('DB 스키마가 아직 작성되지 않았습니다.');
21
- return;
22
- }
23
- formatOutput(data, globalOpts);
24
- } catch (error) {
25
- // Error already handled
26
- }
27
- });
28
-
29
- // ch db-schema set
30
- dbSchema
31
- .command('set')
32
- .description('DB 스키마 생성/수정')
33
- .requiredOption('--title <title>', 'DB 스키마 제목')
34
- .requiredOption('--content <content>', 'DB 스키마 본문 (마크다운)')
35
- .option('--db-type <dbType>', 'DB 유형 (firestore, supabase, mysql, generic)', 'generic')
36
- .option('--new-version', '새 버전으로 기록')
37
- .action(async (options) => {
38
- const globalOpts = program.opts();
39
- try {
40
- if (globalOpts.dryRun) {
41
- printInfo(`[DRY-RUN] DB 스키마 "${options.title}" 생성/수정 요청`);
42
- return;
43
- }
44
- const client = createClient({ projectId: globalOpts.project });
45
- const params: any = {};
46
- if (options.newVersion) params.newVersion = 'true';
47
-
48
- const { data } = await client.put('/db-schema', {
49
- title: options.title,
50
- content: options.content,
51
- dbType: options.dbType,
52
- }, { params });
53
- formatOutput(data, globalOpts);
54
- printSuccess(data.message || 'DB 스키마가 저장되었습니다.');
55
- } catch (error) {
56
- // Error already handled
57
- }
58
- });
59
-
60
- // ch db-schema versions
61
- dbSchema
62
- .command('versions')
63
- .description('DB 스키마 버전 이력 조회')
64
- .action(async () => {
65
- const globalOpts = program.opts();
66
- try {
67
- const client = createClient({ projectId: globalOpts.project });
68
- const { data } = await client.get('/db-schema/versions');
69
- formatOutput(data, globalOpts);
70
- } catch (error) {
71
- // Error already handled
72
- }
73
- });
74
- }