@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.
@@ -1,422 +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 registerSqaCommand(program: Command): void {
9
- const sqa = program
10
- .command('sqa')
11
- .description('SQA 관리 (시트 + 수행)');
12
-
13
- // ══════════════════════════════════════════════════
14
- // Sheet (시트 템플릿) commands
15
- // ══════════════════════════════════════════════════
16
-
17
- // ch sqa list — 시트 목록
18
- sqa
19
- .command('list')
20
- .description('SQA 시트 목록 조회')
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.sort) params.sortBy = options.sort;
30
- if (options.desc) params.sortOrder = 'desc';
31
- params.page = parseInt(options.page, 10);
32
-
33
- const { data } = await client.get('/sqa', { params });
34
- formatOutput(data, globalOpts);
35
- } catch (error) {
36
- // Error already handled
37
- }
38
- });
39
-
40
- // ch sqa get <sheetId> — 시트 상세
41
- sqa
42
- .command('get <sheetId>')
43
- .description('SQA 시트 상세 조회')
44
- .action(async (sheetId: string) => {
45
- const globalOpts = program.opts();
46
- try {
47
- const client = createClient({ projectId: globalOpts.project });
48
- const { data } = await client.get(`/sqa/${sheetId}`);
49
- formatOutput(data, globalOpts);
50
- } catch (error) {
51
- // Error already handled
52
- }
53
- });
54
-
55
- // ch sqa create — 시트 생성
56
- sqa
57
- .command('create')
58
- .description('SQA 시트 생성')
59
- .requiredOption('--name <name>', '시트명')
60
- .action(async (options) => {
61
- const globalOpts = program.opts();
62
- try {
63
- if (globalOpts.dryRun) {
64
- printInfo(`[DRY-RUN] SQA 시트 "${options.name}" 생성 요청`);
65
- return;
66
- }
67
- const client = createClient({ projectId: globalOpts.project });
68
- const { data } = await client.post('/sqa', {
69
- name: options.name,
70
- });
71
- formatOutput(data, globalOpts);
72
- printSuccess('SQA 시트가 생성되었습니다.');
73
- } catch (error) {
74
- // Error already handled
75
- }
76
- });
77
-
78
- // ch sqa update <sheetId> — 시트 수정
79
- sqa
80
- .command('update <sheetId>')
81
- .description('SQA 시트 수정')
82
- .option('--name <name>', '시트명')
83
- .action(async (sheetId: string, options) => {
84
- const globalOpts = program.opts();
85
- try {
86
- if (globalOpts.dryRun) {
87
- printInfo(`[DRY-RUN] SQA 시트 "${sheetId}" 수정 요청`);
88
- return;
89
- }
90
- const body: any = {};
91
- if (options.name) body.name = options.name;
92
-
93
- if (Object.keys(body).length === 0) {
94
- printError('수정할 필드를 지정하세요 (--name).');
95
- return;
96
- }
97
-
98
- const client = createClient({ projectId: globalOpts.project });
99
- const { data } = await client.patch(`/sqa/${sheetId}`, body);
100
- formatOutput(data, globalOpts);
101
- printSuccess('SQA 시트가 수정되었습니다.');
102
- } catch (error) {
103
- // Error already handled
104
- }
105
- });
106
-
107
- // ch sqa delete <sheetId> — 시트 삭제
108
- sqa
109
- .command('delete <sheetId>')
110
- .description('SQA 시트 삭제')
111
- .action(async (sheetId: string) => {
112
- const globalOpts = program.opts();
113
- try {
114
- if (globalOpts.dryRun) {
115
- printInfo(`[DRY-RUN] SQA 시트 "${sheetId}" 삭제 요청`);
116
- return;
117
- }
118
- const client = createClient({ projectId: globalOpts.project });
119
- await client.delete(`/sqa/${sheetId}`);
120
- printSuccess('SQA 시트가 삭제되었습니다.');
121
- } catch (error) {
122
- // Error already handled
123
- }
124
- });
125
-
126
- // ch sqa add-item <sheetId> — 시트에 테스트 항목 추가
127
- sqa
128
- .command('add-item <sheetId>')
129
- .description('SQA 시트에 테스트 항목 추가')
130
- .requiredOption('--test <testItem>', '테스트 항목명')
131
- .option('--spec <specId>', '관련 기능명세 ID')
132
- .action(async (sheetId: string, options) => {
133
- const globalOpts = program.opts();
134
- try {
135
- if (globalOpts.dryRun) {
136
- printInfo(`[DRY-RUN] SQA 시트 "${sheetId}"에 항목 추가 요청`);
137
- return;
138
- }
139
- const client = createClient({ projectId: globalOpts.project });
140
- const { data } = await client.post(`/sqa/${sheetId}/items`, {
141
- testItem: options.test,
142
- relatedSpec: options.spec || '',
143
- });
144
- formatOutput(data, globalOpts);
145
- printSuccess('테스트 항목이 추가되었습니다.');
146
- } catch (error) {
147
- // Error already handled
148
- }
149
- });
150
-
151
- // ══════════════════════════════════════════════════
152
- // Run (수행) commands
153
- // ══════════════════════════════════════════════════
154
-
155
- // ch sqa start-run <sheetId> — 시트 기반 수행 시작
156
- sqa
157
- .command('start-run <sheetId>')
158
- .description('시트 기반 SQA 수행 시작 (항목을 스냅샷하여 Run 생성)')
159
- .requiredOption('--date <date>', '수행일 (YYYY-MM-DD)')
160
- .action(async (sheetId: string, options) => {
161
- const globalOpts = program.opts();
162
- try {
163
- if (globalOpts.dryRun) {
164
- printInfo(`[DRY-RUN] SQA 시트 "${sheetId}" 기반 수행 시작 요청`);
165
- return;
166
- }
167
- const client = createClient({ projectId: globalOpts.project });
168
- const { data } = await client.post(`/sqa/${sheetId}/start-run`, {
169
- performDate: options.date,
170
- });
171
- formatOutput(data, globalOpts);
172
- printSuccess('SQA 수행이 시작되었습니다.');
173
- } catch (error) {
174
- // Error already handled
175
- }
176
- });
177
-
178
- // ch sqa runs — 수행 목록
179
- sqa
180
- .command('runs')
181
- .description('SQA 수행 목록 조회')
182
- .option('--sort <field>', '정렬 필드')
183
- .option('--desc', '내림차순 정렬')
184
- .option('--page <number>', '페이지 번호', '1')
185
- .action(async (options) => {
186
- const globalOpts = program.opts();
187
- try {
188
- const client = createClient({ projectId: globalOpts.project });
189
- const params: any = {};
190
- if (options.sort) params.sortBy = options.sort;
191
- if (options.desc) params.sortOrder = 'desc';
192
- params.page = parseInt(options.page, 10);
193
-
194
- const { data } = await client.get('/sqa/runs/list', { params });
195
- formatOutput(data, globalOpts);
196
- } catch (error) {
197
- // Error already handled
198
- }
199
- });
200
-
201
- // ch sqa run <runId> — 수행 상세
202
- sqa
203
- .command('run <runId>')
204
- .description('SQA 수행 상세 조회')
205
- .action(async (runId: string) => {
206
- const globalOpts = program.opts();
207
- try {
208
- const client = createClient({ projectId: globalOpts.project });
209
- const { data } = await client.get(`/sqa/runs/${runId}`);
210
- formatOutput(data, globalOpts);
211
- } catch (error) {
212
- // Error already handled
213
- }
214
- });
215
-
216
- // ch sqa check <runId> <itemId> — 수행 항목 체크
217
- sqa
218
- .command('check <runId> <itemId>')
219
- .description('SQA 수행 항목 검토결과 체크')
220
- .requiredOption('--result <result>', '결과 (yes|no)')
221
- .option('--note <note>', '비고')
222
- .action(async (runId: string, itemId: string, options) => {
223
- const globalOpts = program.opts();
224
- try {
225
- if (globalOpts.dryRun) {
226
- printInfo(`[DRY-RUN] SQA Run "${runId}" 항목 "${itemId}" 체크 요청`);
227
- return;
228
- }
229
- const client = createClient({ projectId: globalOpts.project });
230
- const body: any = { result: options.result };
231
- if (options.note) body.note = options.note;
232
-
233
- const { data } = await client.patch(`/sqa/runs/${runId}/items/${itemId}/check`, body);
234
- formatOutput(data, globalOpts);
235
- printSuccess('검토결과가 체크되었습니다.');
236
- } catch (error) {
237
- // Error already handled
238
- }
239
- });
240
-
241
- // ch sqa check-bulk <runId> — 수행 항목 일괄 체크
242
- sqa
243
- .command('check-bulk <runId>')
244
- .description('SQA 수행 여러 항목 일괄 체크')
245
- .requiredOption('--file <filePath>', '결과 JSON 파일 경로')
246
- .action(async (runId: string, options) => {
247
- const globalOpts = program.opts();
248
- try {
249
- if (globalOpts.dryRun) {
250
- printInfo(`[DRY-RUN] SQA Run "${runId}" 일괄 체크 요청`);
251
- return;
252
- }
253
-
254
- const filePath = path.resolve(options.file);
255
- if (!fs.existsSync(filePath)) {
256
- printError(`파일을 찾을 수 없습니다: ${filePath}`);
257
- return;
258
- }
259
-
260
- const fileContent = fs.readFileSync(filePath, 'utf-8');
261
- let results: any;
262
- try {
263
- results = JSON.parse(fileContent);
264
- } catch {
265
- printError('파일이 올바른 JSON 형식이 아닙니다.');
266
- return;
267
- }
268
-
269
- const client = createClient({ projectId: globalOpts.project });
270
- const { data } = await client.post(`/sqa/runs/${runId}/items/check-bulk`, results);
271
- formatOutput(data, globalOpts);
272
- printSuccess('일괄 체크가 완료되었습니다.');
273
- } catch (error) {
274
- // Error already handled
275
- }
276
- });
277
-
278
- // ch sqa complete <runId> — 수행 완료
279
- sqa
280
- .command('complete <runId>')
281
- .description('SQA 수행 완료')
282
- .action(async (runId: string) => {
283
- const globalOpts = program.opts();
284
- try {
285
- if (globalOpts.dryRun) {
286
- printInfo(`[DRY-RUN] SQA Run "${runId}" 수행 완료 요청`);
287
- return;
288
- }
289
- const client = createClient({ projectId: globalOpts.project });
290
- const { data } = await client.post(`/sqa/runs/${runId}/complete`);
291
- formatOutput(data, globalOpts);
292
- printSuccess('SQA 수행이 완료되었습니다.');
293
- } catch (error) {
294
- // Error already handled
295
- }
296
- });
297
-
298
- // ch sqa summary <runId> — 수행 집계
299
- sqa
300
- .command('summary <runId>')
301
- .description('SQA 수행 Pass/Fail 집계 조회')
302
- .action(async (runId: string) => {
303
- const globalOpts = program.opts();
304
- try {
305
- const client = createClient({ projectId: globalOpts.project });
306
- const { data } = await client.get(`/sqa/runs/${runId}/summary`);
307
- formatOutput(data, globalOpts);
308
- } catch (error) {
309
- // Error already handled
310
- }
311
- });
312
-
313
- // ch sqa export <runId> — 수행 엑셀 Export
314
- sqa
315
- .command('export <runId>')
316
- .description('SQA 수행 엑셀 Export')
317
- .requiredOption('--output <filePath>', '출력 파일 경로')
318
- .action(async (runId: string, options) => {
319
- const globalOpts = program.opts();
320
- try {
321
- const client = createClient({ projectId: globalOpts.project });
322
- const response = await client.get(`/sqa/runs/${runId}/export`, {
323
- responseType: 'arraybuffer',
324
- });
325
-
326
- const outputPath = path.resolve(options.output);
327
- fs.writeFileSync(outputPath, Buffer.from(response.data));
328
- printSuccess(`엑셀 파일이 저장되었습니다: ${outputPath}`);
329
- } catch (error) {
330
- // Error already handled
331
- }
332
- });
333
-
334
- // ch sqa delete-run <runId> — 수행 삭제
335
- sqa
336
- .command('delete-run <runId>')
337
- .description('SQA 수행 삭제')
338
- .action(async (runId: string) => {
339
- const globalOpts = program.opts();
340
- try {
341
- if (globalOpts.dryRun) {
342
- printInfo(`[DRY-RUN] SQA Run "${runId}" 삭제 요청`);
343
- return;
344
- }
345
- const client = createClient({ projectId: globalOpts.project });
346
- await client.delete(`/sqa/runs/${runId}`);
347
- printSuccess('SQA 수행이 삭제되었습니다.');
348
- } catch (error) {
349
- // Error already handled
350
- }
351
- });
352
-
353
- // ══════════════════════════════════════════════════
354
- // Import (엑셀 → 시트)
355
- // ══════════════════════════════════════════════════
356
-
357
- sqa
358
- .command('import')
359
- .description('SQA 엑셀 Import (시트 생성)')
360
- .requiredOption('--file <filePath>', '엑셀 파일 경로')
361
- .option('--name <name>', '시트명')
362
- .option('--date <date>', '수행일')
363
- .action(async (options) => {
364
- const globalOpts = program.opts();
365
- try {
366
- if (globalOpts.dryRun) {
367
- printInfo('[DRY-RUN] SQA 엑셀 Import 요청');
368
- return;
369
- }
370
-
371
- const filePath = path.resolve(options.file);
372
- if (!fs.existsSync(filePath)) {
373
- printError(`파일을 찾을 수 없습니다: ${filePath}`);
374
- return;
375
- }
376
-
377
- const form = new FormData();
378
- form.append('file', fs.createReadStream(filePath));
379
- if (options.name) form.append('name', options.name);
380
- if (options.date) form.append('performDate', options.date);
381
-
382
- const client = createClient({ projectId: globalOpts.project });
383
- const { data } = await client.post('/sqa/import', form, {
384
- headers: {
385
- ...form.getHeaders(),
386
- },
387
- });
388
- formatOutput(data, globalOpts);
389
- printSuccess('SQA 엑셀 Import가 완료되었습니다.');
390
- } catch (error) {
391
- // Error already handled
392
- }
393
- });
394
-
395
- sqa
396
- .command('import-preview')
397
- .description('SQA 엑셀 Import 미리보기')
398
- .requiredOption('--file <filePath>', '엑셀 파일 경로')
399
- .action(async (options) => {
400
- const globalOpts = program.opts();
401
- try {
402
- const filePath = path.resolve(options.file);
403
- if (!fs.existsSync(filePath)) {
404
- printError(`파일을 찾을 수 없습니다: ${filePath}`);
405
- return;
406
- }
407
-
408
- const form = new FormData();
409
- form.append('file', fs.createReadStream(filePath));
410
-
411
- const client = createClient({ projectId: globalOpts.project });
412
- const { data } = await client.post('/sqa/import-preview', form, {
413
- headers: {
414
- ...form.getHeaders(),
415
- },
416
- });
417
- formatOutput(data, globalOpts);
418
- } catch (error) {
419
- // Error already handled
420
- }
421
- });
422
- }
package/src/config.ts DELETED
@@ -1,98 +0,0 @@
1
- import * as fs from 'fs';
2
- import * as path from 'path';
3
- import * as os from 'os';
4
-
5
- export interface ChConfig {
6
- apiKey: string;
7
- projectId: string;
8
- apiUrl: string;
9
- defaultFormat: 'table' | 'json' | 'csv';
10
- }
11
-
12
- export interface ChProjectLocal {
13
- projectId: string;
14
- projectName?: string;
15
- }
16
-
17
- export interface ProjectContext {
18
- projectId: string;
19
- projectName?: string;
20
- source: 'cli-option' | 'local' | 'global';
21
- }
22
-
23
- const CONFIG_DIR = path.join(os.homedir(), '.ch');
24
- const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
25
- const LOCAL_PROJECT_FILE = '.ch-project';
26
-
27
- export function loadConfig(): ChConfig | null {
28
- try {
29
- if (!fs.existsSync(CONFIG_FILE)) {
30
- return null;
31
- }
32
- const raw = fs.readFileSync(CONFIG_FILE, 'utf-8');
33
- return JSON.parse(raw) as ChConfig;
34
- } catch {
35
- return null;
36
- }
37
- }
38
-
39
- /**
40
- * Search for .ch-project file from cwd upward.
41
- */
42
- export function findLocalProject(): ChProjectLocal | null {
43
- try {
44
- let dir = process.cwd();
45
- const root = path.parse(dir).root;
46
- while (true) {
47
- const filePath = path.join(dir, LOCAL_PROJECT_FILE);
48
- if (fs.existsSync(filePath)) {
49
- const raw = fs.readFileSync(filePath, 'utf-8');
50
- return JSON.parse(raw) as ChProjectLocal;
51
- }
52
- const parent = path.dirname(dir);
53
- if (parent === dir || dir === root) break;
54
- dir = parent;
55
- }
56
- } catch {
57
- // ignore parse errors
58
- }
59
- return null;
60
- }
61
-
62
- /**
63
- * Resolve projectId with priority: CLI --project > .ch-project > ~/.ch/config.json
64
- */
65
- export function getProjectContext(cliProjectId?: string): ProjectContext | null {
66
- if (cliProjectId) {
67
- return { projectId: cliProjectId, source: 'cli-option' };
68
- }
69
-
70
- const local = findLocalProject();
71
- if (local?.projectId) {
72
- return { projectId: local.projectId, projectName: local.projectName, source: 'local' };
73
- }
74
-
75
- const global = loadConfig();
76
- if (global?.projectId) {
77
- return { projectId: global.projectId, source: 'global' };
78
- }
79
-
80
- return null;
81
- }
82
-
83
- export function saveConfig(config: ChConfig): void {
84
- if (!fs.existsSync(CONFIG_DIR)) {
85
- fs.mkdirSync(CONFIG_DIR, { recursive: true });
86
- }
87
- fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), 'utf-8');
88
- }
89
-
90
- export function clearConfig(): void {
91
- if (fs.existsSync(CONFIG_FILE)) {
92
- fs.unlinkSync(CONFIG_FILE);
93
- }
94
- }
95
-
96
- export function getConfigPath(): string {
97
- return CONFIG_FILE;
98
- }
package/src/index.ts DELETED
@@ -1,69 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { Command } from 'commander';
4
- import * as path from 'path';
5
- import * as fs from 'fs';
6
-
7
- import { registerAuthCommand } from './commands/auth';
8
- import { registerInitCommand } from './commands/init';
9
- import { registerProjectsCommand } from './commands/projects';
10
- import { registerSpecsCommand } from './commands/specs';
11
- import { registerSprintsCommand } from './commands/sprints';
12
- import { registerQnaCommand } from './commands/qna';
13
- import { registerSqaCommand } from './commands/sqa';
14
- import { registerArchivesCommand } from './commands/archives';
15
- import { registerMembersCommand } from './commands/members';
16
- import { registerDevStatusCommand } from './commands/dev-status';
17
- import { registerPrdCommand } from './commands/prd';
18
- import { registerDbSchemaCommand } from './commands/db-schema';
19
-
20
- import { registerNotificationsCommand } from './commands/notifications';
21
- import { registerDashboardCommand } from './commands/dashboard';
22
-
23
- // Read version from package.json
24
- function getVersion(): string {
25
- try {
26
- const pkgPath = path.join(__dirname, '..', 'package.json');
27
- const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
28
- return pkg.version || '1.0.0';
29
- } catch {
30
- return '1.0.0';
31
- }
32
- }
33
-
34
- const program = new Command();
35
-
36
- program
37
- .name('ch')
38
- .description('Decencia Communication Channel CLI')
39
- .version(getVersion(), '-v, --version', '버전 표시')
40
- .option('--json', 'JSON 형식으로 출력')
41
- .option('--table', '테이블 형식으로 출력 (기본값)')
42
- .option('--csv', 'CSV 형식으로 출력')
43
- .option('--project <projectId>', '프로젝트 ID 오버라이드')
44
- .option('--verbose', '상세 로그 출력')
45
- .option('--quiet', '결과만 출력 (스크립팅용)')
46
- .option('--dry-run', '쓰기 작업 시뮬레이션');
47
-
48
- // Register all command modules
49
- registerInitCommand(program);
50
- registerAuthCommand(program);
51
- registerProjectsCommand(program);
52
- registerSpecsCommand(program);
53
- registerSprintsCommand(program);
54
- registerQnaCommand(program);
55
- registerSqaCommand(program);
56
- registerArchivesCommand(program);
57
- registerMembersCommand(program);
58
- registerDevStatusCommand(program);
59
- registerPrdCommand(program);
60
- registerDbSchemaCommand(program);
61
-
62
- registerNotificationsCommand(program);
63
- registerDashboardCommand(program);
64
-
65
- // Parse and execute
66
- program.parseAsync(process.argv).catch((err) => {
67
- console.error(err);
68
- process.exit(1);
69
- });