@decencia/ch-cli 1.0.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.
Files changed (101) hide show
  1. package/dist/client.d.ts +15 -0
  2. package/dist/client.d.ts.map +1 -0
  3. package/dist/client.js +117 -0
  4. package/dist/client.js.map +1 -0
  5. package/dist/commands/archives.d.ts +3 -0
  6. package/dist/commands/archives.d.ts.map +1 -0
  7. package/dist/commands/archives.js +371 -0
  8. package/dist/commands/archives.js.map +1 -0
  9. package/dist/commands/auth.d.ts +3 -0
  10. package/dist/commands/auth.d.ts.map +1 -0
  11. package/dist/commands/auth.js +149 -0
  12. package/dist/commands/auth.js.map +1 -0
  13. package/dist/commands/dashboard.d.ts +3 -0
  14. package/dist/commands/dashboard.d.ts.map +1 -0
  15. package/dist/commands/dashboard.js +115 -0
  16. package/dist/commands/dashboard.js.map +1 -0
  17. package/dist/commands/db-schema.d.ts +3 -0
  18. package/dist/commands/db-schema.d.ts.map +1 -0
  19. package/dist/commands/db-schema.js +76 -0
  20. package/dist/commands/db-schema.js.map +1 -0
  21. package/dist/commands/dev-status.d.ts +3 -0
  22. package/dist/commands/dev-status.d.ts.map +1 -0
  23. package/dist/commands/dev-status.js +175 -0
  24. package/dist/commands/dev-status.js.map +1 -0
  25. package/dist/commands/init.d.ts +3 -0
  26. package/dist/commands/init.d.ts.map +1 -0
  27. package/dist/commands/init.js +124 -0
  28. package/dist/commands/init.js.map +1 -0
  29. package/dist/commands/invitations.d.ts +3 -0
  30. package/dist/commands/invitations.d.ts.map +1 -0
  31. package/dist/commands/invitations.js +48 -0
  32. package/dist/commands/invitations.js.map +1 -0
  33. package/dist/commands/members.d.ts +3 -0
  34. package/dist/commands/members.d.ts.map +1 -0
  35. package/dist/commands/members.js +93 -0
  36. package/dist/commands/members.js.map +1 -0
  37. package/dist/commands/notifications.d.ts +3 -0
  38. package/dist/commands/notifications.d.ts.map +1 -0
  39. package/dist/commands/notifications.js +163 -0
  40. package/dist/commands/notifications.js.map +1 -0
  41. package/dist/commands/prd.d.ts +3 -0
  42. package/dist/commands/prd.d.ts.map +1 -0
  43. package/dist/commands/prd.js +74 -0
  44. package/dist/commands/prd.js.map +1 -0
  45. package/dist/commands/projects.d.ts +3 -0
  46. package/dist/commands/projects.d.ts.map +1 -0
  47. package/dist/commands/projects.js +215 -0
  48. package/dist/commands/projects.js.map +1 -0
  49. package/dist/commands/qna.d.ts +3 -0
  50. package/dist/commands/qna.d.ts.map +1 -0
  51. package/dist/commands/qna.js +211 -0
  52. package/dist/commands/qna.js.map +1 -0
  53. package/dist/commands/specs.d.ts +3 -0
  54. package/dist/commands/specs.d.ts.map +1 -0
  55. package/dist/commands/specs.js +277 -0
  56. package/dist/commands/specs.js.map +1 -0
  57. package/dist/commands/sprints.d.ts +3 -0
  58. package/dist/commands/sprints.d.ts.map +1 -0
  59. package/dist/commands/sprints.js +262 -0
  60. package/dist/commands/sprints.js.map +1 -0
  61. package/dist/commands/sqa.d.ts +3 -0
  62. package/dist/commands/sqa.d.ts.map +1 -0
  63. package/dist/commands/sqa.js +452 -0
  64. package/dist/commands/sqa.js.map +1 -0
  65. package/dist/config.d.ts +28 -0
  66. package/dist/config.d.ts.map +1 -0
  67. package/dist/config.js +115 -0
  68. package/dist/config.js.map +1 -0
  69. package/dist/index.d.ts +3 -0
  70. package/dist/index.d.ts.map +1 -0
  71. package/dist/index.js +97 -0
  72. package/dist/index.js.map +1 -0
  73. package/dist/output.d.ts +13 -0
  74. package/dist/output.d.ts.map +1 -0
  75. package/dist/output.js +174 -0
  76. package/dist/output.js.map +1 -0
  77. package/dist/utils.d.ts +2 -0
  78. package/dist/utils.d.ts.map +1 -0
  79. package/dist/utils.js +7 -0
  80. package/dist/utils.js.map +1 -0
  81. package/package.json +33 -0
  82. package/src/client.ts +137 -0
  83. package/src/commands/archives.ts +334 -0
  84. package/src/commands/auth.ts +154 -0
  85. package/src/commands/dashboard.ts +112 -0
  86. package/src/commands/db-schema.ts +74 -0
  87. package/src/commands/dev-status.ts +166 -0
  88. package/src/commands/init.ts +92 -0
  89. package/src/commands/members.ts +92 -0
  90. package/src/commands/notifications.ts +169 -0
  91. package/src/commands/prd.ts +72 -0
  92. package/src/commands/projects.ts +207 -0
  93. package/src/commands/qna.ts +205 -0
  94. package/src/commands/specs.ts +257 -0
  95. package/src/commands/sprints.ts +255 -0
  96. package/src/commands/sqa.ts +422 -0
  97. package/src/config.ts +98 -0
  98. package/src/index.ts +69 -0
  99. package/src/output.ts +186 -0
  100. package/src/utils.ts +3 -0
  101. package/tsconfig.json +18 -0
@@ -0,0 +1,166 @@
1
+ import { Command } from 'commander';
2
+ import { createClient } from '../client';
3
+ import { formatOutput, printSuccess, printError, printInfo } from '../output';
4
+ import { parseCommaSeparated } from '../utils';
5
+
6
+ export function registerDevStatusCommand(program: Command): void {
7
+ const devStatus = program
8
+ .command('dev-status')
9
+ .description('개발현황 문서 관리');
10
+
11
+ // ch dev-status list
12
+ devStatus
13
+ .command('list')
14
+ .description('개발현황 문서 목록 조회')
15
+ .option('--sprint <sprintId>', '관련 스프린트 ID 필터')
16
+ .option('--spec <specId>', '관련 기능명세 ID 필터')
17
+ .option('--search <keyword>', '제목 검색')
18
+ .option('--sort <field>', '정렬 필드')
19
+ .option('--desc', '내림차순 정렬')
20
+ .option('--page <number>', '페이지 번호', '1')
21
+ .option('--per-page <number>', '페이지당 건수', '20')
22
+ .action(async (options) => {
23
+ const globalOpts = program.opts();
24
+ try {
25
+ const client = createClient({ projectId: globalOpts.project });
26
+ const params: any = {};
27
+ if (options.sprint) params.relatedSprintId = options.sprint;
28
+ if (options.spec) params.relatedSpecId = options.spec;
29
+ if (options.search) params.search = options.search;
30
+ if (options.sort) params.sortBy = options.sort;
31
+ if (options.desc) params.sortOrder = 'desc';
32
+ params.page = parseInt(options.page, 10);
33
+ params.perPage = parseInt(options.perPage, 10);
34
+
35
+ const { data } = await client.get('/dev-status', { params });
36
+ formatOutput(data, globalOpts);
37
+ } catch (error) {
38
+ // Error already handled
39
+ }
40
+ });
41
+
42
+ // ch dev-status get
43
+ devStatus
44
+ .command('get <docId>')
45
+ .description('개발현황 문서 상세 조회')
46
+ .action(async (docId: string) => {
47
+ const globalOpts = program.opts();
48
+ try {
49
+ const client = createClient({ projectId: globalOpts.project });
50
+ const { data } = await client.get(`/dev-status/${docId}`);
51
+ formatOutput(data, globalOpts);
52
+ } catch (error) {
53
+ // Error already handled
54
+ }
55
+ });
56
+
57
+ // ch dev-status create
58
+ devStatus
59
+ .command('create')
60
+ .description('개발현황 문서 생성')
61
+ .requiredOption('--title <title>', '문서 제목')
62
+ .option('--content <content>', '본문 (HTML)')
63
+ .option('--sprints <sprintIds>', '관련 스프린트 ID (쉼표 구분)')
64
+ .option('--specs <specIds>', '관련 기능명세 ID (쉼표 구분)')
65
+ .action(async (options) => {
66
+ const globalOpts = program.opts();
67
+ try {
68
+ if (globalOpts.dryRun) {
69
+ printInfo(`[DRY-RUN] 개발현황 문서 "${options.title}" 생성 요청`);
70
+ return;
71
+ }
72
+ const client = createClient({ projectId: globalOpts.project });
73
+ const body: any = {
74
+ title: options.title,
75
+ };
76
+ if (options.content) body.content = options.content;
77
+ if (options.sprints) body.relatedSprintIds = parseCommaSeparated(options.sprints);
78
+ if (options.specs) body.relatedSpecIds = parseCommaSeparated(options.specs);
79
+
80
+ const { data } = await client.post('/dev-status', body);
81
+ formatOutput(data, globalOpts);
82
+ printSuccess('개발현황 문서가 생성되었습니다.');
83
+ } catch (error) {
84
+ // Error already handled
85
+ }
86
+ });
87
+
88
+ // ch dev-status update
89
+ devStatus
90
+ .command('update <docId>')
91
+ .description('개발현황 문서 수정')
92
+ .option('--title <title>', '문서 제목')
93
+ .option('--content <content>', '본문 (HTML)')
94
+ .option('--sprints <sprintIds>', '관련 스프린트 ID (쉼표 구분)')
95
+ .option('--specs <specIds>', '관련 기능명세 ID (쉼표 구분)')
96
+ .option('--change-note <note>', '변경 사유')
97
+ .option('--new-version', '새 버전으로 수정')
98
+ .action(async (docId: string, options) => {
99
+ const globalOpts = program.opts();
100
+ try {
101
+ if (globalOpts.dryRun) {
102
+ printInfo(`[DRY-RUN] 개발현황 문서 "${docId}" 수정 요청`);
103
+ return;
104
+ }
105
+ const client = createClient({ projectId: globalOpts.project });
106
+ const body: any = {};
107
+ if (options.title) body.title = options.title;
108
+ if (options.content) body.content = options.content;
109
+ if (options.sprints) body.relatedSprintIds = parseCommaSeparated(options.sprints);
110
+ if (options.specs) body.relatedSpecIds = parseCommaSeparated(options.specs);
111
+ if (options.changeNote) body.changeNote = options.changeNote;
112
+
113
+ if (Object.keys(body).length === 0) {
114
+ printError('수정할 필드를 지정하세요.');
115
+ return;
116
+ }
117
+
118
+ const params: any = {};
119
+ if (options.newVersion) params.newVersion = 'true';
120
+
121
+ const { data } = await client.patch(`/dev-status/${docId}`, body, { params });
122
+ formatOutput(data, globalOpts);
123
+ printSuccess(
124
+ options.newVersion
125
+ ? '개발현황 문서가 새 버전으로 수정되었습니다.'
126
+ : '개발현황 문서가 수정되었습니다.',
127
+ );
128
+ } catch (error) {
129
+ // Error already handled
130
+ }
131
+ });
132
+
133
+ // ch dev-status delete
134
+ devStatus
135
+ .command('delete <docId>')
136
+ .description('개발현황 문서 삭제')
137
+ .action(async (docId: string) => {
138
+ const globalOpts = program.opts();
139
+ try {
140
+ if (globalOpts.dryRun) {
141
+ printInfo(`[DRY-RUN] 개발현황 문서 "${docId}" 삭제 요청`);
142
+ return;
143
+ }
144
+ const client = createClient({ projectId: globalOpts.project });
145
+ await client.delete(`/dev-status/${docId}`);
146
+ printSuccess('개발현황 문서가 삭제되었습니다.');
147
+ } catch (error) {
148
+ // Error already handled
149
+ }
150
+ });
151
+
152
+ // ch dev-status versions
153
+ devStatus
154
+ .command('versions <docId>')
155
+ .description('개발현황 문서 버전 이력 조회')
156
+ .action(async (docId: string) => {
157
+ const globalOpts = program.opts();
158
+ try {
159
+ const client = createClient({ projectId: globalOpts.project });
160
+ const { data } = await client.get(`/dev-status/${docId}/versions`);
161
+ formatOutput(data, globalOpts);
162
+ } catch (error) {
163
+ // Error already handled
164
+ }
165
+ });
166
+ }
@@ -0,0 +1,92 @@
1
+ import { Command } from 'commander';
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+ import { createClient } from '../client';
5
+ import { printSuccess, printError, printInfo } from '../output';
6
+ import { loadConfig, findLocalProject } from '../config';
7
+
8
+ export function registerInitCommand(program: Command): void {
9
+ program
10
+ .command('init')
11
+ .description('현재 디렉토리에 .ch-project 파일 생성 (로컬 프로젝트 설정)')
12
+ .requiredOption('--project-id <projectId>', '프로젝트 ID')
13
+ .option('--project-name <projectName>', '프로젝트명 (표시용)')
14
+ .action(async (options) => {
15
+ const targetPath = path.join(process.cwd(), '.ch-project');
16
+
17
+ if (fs.existsSync(targetPath)) {
18
+ try {
19
+ const existing = JSON.parse(fs.readFileSync(targetPath, 'utf-8'));
20
+ printError(
21
+ `.ch-project 파일이 이미 존재합니다 (projectId: ${existing.projectId}). 덮어쓰려면 파일을 삭제 후 다시 실행하세요.`
22
+ );
23
+ } catch {
24
+ printError('.ch-project 파일이 이미 존재합니다. 덮어쓰려면 파일을 삭제 후 다시 실행하세요.');
25
+ }
26
+ return;
27
+ }
28
+
29
+ // If projectName not given, try to fetch from API (only if API key is available)
30
+ let projectName = options.projectName;
31
+ if (!projectName) {
32
+ const config = loadConfig();
33
+ if (config?.apiKey) {
34
+ try {
35
+ const client = createClient({ projectId: options.projectId });
36
+ const { data } = await client.get('/projects/current');
37
+ projectName = data.name;
38
+ } catch {
39
+ // API call failed, proceed without name
40
+ }
41
+ }
42
+ }
43
+
44
+ const content: any = { projectId: options.projectId };
45
+ if (projectName) content.projectName = projectName;
46
+
47
+ fs.writeFileSync(targetPath, JSON.stringify(content, null, 2) + '\n', 'utf-8');
48
+ printSuccess(`✅ .ch-project 파일이 생성되었습니다.`);
49
+ printInfo(` projectId: ${options.projectId}`);
50
+ if (projectName) {
51
+ printInfo(` projectName: ${projectName}`);
52
+ }
53
+ printInfo(` 경로: ${targetPath}`);
54
+ });
55
+
56
+ // ch status - show current project context
57
+ program
58
+ .command('status')
59
+ .description('현재 프로젝트 설정 상태 확인')
60
+ .action(() => {
61
+ const config = loadConfig();
62
+ const local = findLocalProject();
63
+
64
+ console.log('');
65
+ if (local) {
66
+ printSuccess(`로컬 설정 (.ch-project):`);
67
+ printInfo(` projectId: ${local.projectId}`);
68
+ if (local.projectName) printInfo(` projectName: ${local.projectName}`);
69
+ } else {
70
+ printInfo('로컬 설정 (.ch-project): 없음');
71
+ }
72
+
73
+ console.log('');
74
+ if (config) {
75
+ printSuccess('글로벌 설정 (~/.ch/config.json):');
76
+ printInfo(` apiUrl: ${config.apiUrl}`);
77
+ printInfo(` projectId: ${config.projectId || '(없음)'}`);
78
+ printInfo(` apiKey: ${config.apiKey ? '***설정됨***' : '(없음)'}`);
79
+ } else {
80
+ printInfo('글로벌 설정 (~/.ch/config.json): 없음');
81
+ }
82
+
83
+ console.log('');
84
+ const effectiveProjectId = local?.projectId || config?.projectId;
85
+ const source = local ? '.ch-project (로컬)' : config?.projectId ? '~/.ch/config.json (글로벌)' : '없음';
86
+ if (effectiveProjectId) {
87
+ printSuccess(`→ 현재 사용할 projectId: ${effectiveProjectId} (출처: ${source})`);
88
+ } else {
89
+ printError('프로젝트가 설정되지 않았습니다. "ch init --project-id <id>" 또는 "ch auth login"을 실행하세요.');
90
+ }
91
+ });
92
+ }
@@ -0,0 +1,92 @@
1
+ import { Command } from 'commander';
2
+ import { createClient } from '../client';
3
+ import { formatOutput, printSuccess, printError, printInfo } from '../output';
4
+
5
+ export function registerMembersCommand(program: Command): void {
6
+ const members = program
7
+ .command('members')
8
+ .description('멤버 관리');
9
+
10
+ // ch members list
11
+ members
12
+ .command('list')
13
+ .description('멤버 목록 조회')
14
+ .action(async () => {
15
+ const globalOpts = program.opts();
16
+ try {
17
+ const client = createClient({ projectId: globalOpts.project });
18
+ const { data } = await client.get('/members');
19
+ formatOutput(data, globalOpts);
20
+ } catch (error) {
21
+ // Error already handled
22
+ }
23
+ });
24
+
25
+ // ch members invite
26
+ members
27
+ .command('invite')
28
+ .description('멤버 추가 (이메일로 사용자를 찾아 바로 권한 부여)')
29
+ .requiredOption('--email <email>', '추가할 사용자 이메일')
30
+ .requiredOption('--role <role>', '역할 (developer|client)')
31
+ .action(async (options) => {
32
+ const globalOpts = program.opts();
33
+ try {
34
+ if (globalOpts.dryRun) {
35
+ printInfo(`[DRY-RUN] "${options.email}" 멤버 추가 요청`);
36
+ return;
37
+ }
38
+ const client = createClient({ projectId: globalOpts.project });
39
+ const { data } = await client.post('/members/invite', {
40
+ email: options.email,
41
+ role: options.role,
42
+ });
43
+ formatOutput(data, globalOpts);
44
+ printSuccess(data.message || '멤버가 추가되었습니다.');
45
+ } catch (error) {
46
+ // Error already handled
47
+ }
48
+ });
49
+
50
+ // ch members set-role
51
+ members
52
+ .command('set-role <userId>')
53
+ .description('멤버 역할 변경')
54
+ .requiredOption('--role <role>', '새 역할 (admin|developer|client)')
55
+ .action(async (userId: string, options) => {
56
+ const globalOpts = program.opts();
57
+ try {
58
+ if (globalOpts.dryRun) {
59
+ printInfo(`[DRY-RUN] 멤버 "${userId}" 역할을 "${options.role}"로 변경 요청`);
60
+ return;
61
+ }
62
+ const client = createClient({ projectId: globalOpts.project });
63
+ const { data } = await client.patch(`/members/${userId}/role`, {
64
+ role: options.role,
65
+ });
66
+ formatOutput(data, globalOpts);
67
+ printSuccess(`멤버 역할이 "${options.role}"로 변경되었습니다.`);
68
+ } catch (error) {
69
+ // Error already handled
70
+ }
71
+ });
72
+
73
+ // ch members remove
74
+ members
75
+ .command('remove <userId>')
76
+ .description('멤버 제거')
77
+ .action(async (userId: string) => {
78
+ const globalOpts = program.opts();
79
+ try {
80
+ if (globalOpts.dryRun) {
81
+ printInfo(`[DRY-RUN] 멤버 "${userId}" 제거 요청`);
82
+ return;
83
+ }
84
+ const client = createClient({ projectId: globalOpts.project });
85
+ await client.delete(`/members/${userId}`);
86
+ printSuccess('멤버가 제거되었습니다.');
87
+ } catch (error) {
88
+ // Error already handled
89
+ }
90
+ });
91
+
92
+ }
@@ -0,0 +1,169 @@
1
+ import { Command } from 'commander';
2
+ import { createClient } from '../client';
3
+ import { formatOutput, printSuccess, printError, printInfo } from '../output';
4
+
5
+ export function registerNotificationsCommand(program: Command): void {
6
+ const notifications = program
7
+ .command('notifications')
8
+ .description('알림 설정 관리');
9
+
10
+ // ch notifications get
11
+ notifications
12
+ .command('get')
13
+ .description('현재 알림 설정 조회')
14
+ .action(async () => {
15
+ const globalOpts = program.opts();
16
+ try {
17
+ const client = createClient({ projectId: globalOpts.project });
18
+ const { data } = await client.get('/notifications');
19
+ formatOutput(data, globalOpts);
20
+ } catch (error) {
21
+ // Error already handled
22
+ }
23
+ });
24
+
25
+ // ch notifications set-webhook
26
+ notifications
27
+ .command('set-webhook')
28
+ .description('Webhook URL 설정')
29
+ .option('--kakao <url>', '카카오워크 Webhook URL')
30
+ .option('--slack <url>', 'Slack Webhook URL')
31
+ .action(async (options) => {
32
+ const globalOpts = program.opts();
33
+ try {
34
+ if (!options.kakao && !options.slack) {
35
+ printError('--kakao 또는 --slack 옵션을 지정하세요.');
36
+ return;
37
+ }
38
+
39
+ if (globalOpts.dryRun) {
40
+ printInfo('[DRY-RUN] Webhook URL 설정 요청');
41
+ return;
42
+ }
43
+
44
+ const body: any = {};
45
+ if (options.kakao) body.kakaoworkWebhookUrl = options.kakao;
46
+ if (options.slack) body.slackWebhookUrl = options.slack;
47
+
48
+ const client = createClient({ projectId: globalOpts.project });
49
+ const { data } = await client.patch('/notifications/webhook', body);
50
+ formatOutput(data, globalOpts);
51
+ printSuccess('Webhook URL이 설정되었습니다.');
52
+ } catch (error) {
53
+ // Error already handled
54
+ }
55
+ });
56
+
57
+ // ch notifications remove-webhook
58
+ notifications
59
+ .command('remove-webhook')
60
+ .description('Webhook URL 제거')
61
+ .option('--kakao', '카카오워크 Webhook 제거')
62
+ .option('--slack', 'Slack Webhook 제거')
63
+ .action(async (options) => {
64
+ const globalOpts = program.opts();
65
+ try {
66
+ if (!options.kakao && !options.slack) {
67
+ printError('--kakao 또는 --slack 옵션을 지정하세요.');
68
+ return;
69
+ }
70
+
71
+ if (globalOpts.dryRun) {
72
+ printInfo('[DRY-RUN] Webhook URL 제거 요청');
73
+ return;
74
+ }
75
+
76
+ const client = createClient({ projectId: globalOpts.project });
77
+
78
+ if (options.kakao) {
79
+ await client.delete('/notifications/webhook/kakao');
80
+ printSuccess('카카오워크 Webhook URL이 제거되었습니다.');
81
+ }
82
+ if (options.slack) {
83
+ await client.delete('/notifications/webhook/slack');
84
+ printSuccess('Slack Webhook URL이 제거되었습니다.');
85
+ }
86
+ } catch (error) {
87
+ // Error already handled
88
+ }
89
+ });
90
+
91
+ // ch notifications test
92
+ notifications
93
+ .command('test')
94
+ .description('테스트 알림 발송')
95
+ .option('--kakao', '카카오워크로 테스트')
96
+ .option('--slack', 'Slack으로 테스트')
97
+ .action(async (options) => {
98
+ const globalOpts = program.opts();
99
+ try {
100
+ if (!options.kakao && !options.slack) {
101
+ printError('--kakao 또는 --slack 옵션을 지정하세요.');
102
+ return;
103
+ }
104
+
105
+ const client = createClient({ projectId: globalOpts.project });
106
+
107
+ if (options.kakao) {
108
+ const { data } = await client.post('/notifications/test/kakao');
109
+ formatOutput(data, globalOpts);
110
+ printSuccess('카카오워크 테스트 알림이 발송되었습니다.');
111
+ }
112
+ if (options.slack) {
113
+ const { data } = await client.post('/notifications/test/slack');
114
+ formatOutput(data, globalOpts);
115
+ printSuccess('Slack 테스트 알림이 발송되었습니다.');
116
+ }
117
+ } catch (error) {
118
+ // Error already handled
119
+ }
120
+ });
121
+
122
+ // ch notifications toggle
123
+ notifications
124
+ .command('toggle <eventName>')
125
+ .description('이벤트 알림 ON/OFF 토글')
126
+ .option('--on', '알림 활성화')
127
+ .option('--off', '알림 비활성화')
128
+ .action(async (eventName: string, options) => {
129
+ const globalOpts = program.opts();
130
+ try {
131
+ if (!options.on && !options.off) {
132
+ printError('--on 또는 --off 옵션을 지정하세요.');
133
+ return;
134
+ }
135
+
136
+ if (globalOpts.dryRun) {
137
+ const state = options.on ? 'ON' : 'OFF';
138
+ printInfo(`[DRY-RUN] 이벤트 "${eventName}" ${state} 요청`);
139
+ return;
140
+ }
141
+
142
+ const enabled = !!options.on;
143
+ const client = createClient({ projectId: globalOpts.project });
144
+ const { data } = await client.patch(`/notifications/events/${eventName}`, { enabled });
145
+ formatOutput(data, globalOpts);
146
+ printSuccess(`이벤트 "${eventName}"이 ${enabled ? '활성화' : '비활성화'}되었습니다.`);
147
+ } catch (error) {
148
+ // Error already handled
149
+ }
150
+ });
151
+
152
+ // ch notifications logs
153
+ notifications
154
+ .command('logs')
155
+ .description('알림 발송 로그 조회')
156
+ .option('--page <number>', '페이지 번호', '1')
157
+ .action(async (options) => {
158
+ const globalOpts = program.opts();
159
+ try {
160
+ const client = createClient({ projectId: globalOpts.project });
161
+ const { data } = await client.get('/notifications/logs', {
162
+ params: { page: parseInt(options.page, 10) },
163
+ });
164
+ formatOutput(data, globalOpts);
165
+ } catch (error) {
166
+ // Error already handled
167
+ }
168
+ });
169
+ }
@@ -0,0 +1,72 @@
1
+ import { Command } from 'commander';
2
+ import { createClient } from '../client';
3
+ import { formatOutput, printSuccess, printError, printInfo } from '../output';
4
+
5
+ export function registerPrdCommand(program: Command): void {
6
+ const prd = program
7
+ .command('prd')
8
+ .description('PRD(Product Requirements Document) 관리');
9
+
10
+ // ch prd get
11
+ prd
12
+ .command('get')
13
+ .description('PRD 조회')
14
+ .action(async () => {
15
+ const globalOpts = program.opts();
16
+ try {
17
+ const client = createClient({ projectId: globalOpts.project });
18
+ const { data } = await client.get('/prd');
19
+ if (!data) {
20
+ printInfo('PRD가 아직 작성되지 않았습니다.');
21
+ return;
22
+ }
23
+ formatOutput(data, globalOpts);
24
+ } catch (error) {
25
+ // Error already handled
26
+ }
27
+ });
28
+
29
+ // ch prd set
30
+ prd
31
+ .command('set')
32
+ .description('PRD 생성/수정')
33
+ .requiredOption('--title <title>', 'PRD 제목')
34
+ .requiredOption('--content <content>', 'PRD 본문 (마크다운)')
35
+ .option('--new-version', '새 버전으로 기록')
36
+ .action(async (options) => {
37
+ const globalOpts = program.opts();
38
+ try {
39
+ if (globalOpts.dryRun) {
40
+ printInfo(`[DRY-RUN] PRD "${options.title}" 생성/수정 요청`);
41
+ return;
42
+ }
43
+ const client = createClient({ projectId: globalOpts.project });
44
+ const params: any = {};
45
+ if (options.newVersion) params.newVersion = 'true';
46
+
47
+ const { data } = await client.put('/prd', {
48
+ title: options.title,
49
+ content: options.content,
50
+ }, { params });
51
+ formatOutput(data, globalOpts);
52
+ printSuccess(data.message || 'PRD가 저장되었습니다.');
53
+ } catch (error) {
54
+ // Error already handled
55
+ }
56
+ });
57
+
58
+ // ch prd versions
59
+ prd
60
+ .command('versions')
61
+ .description('PRD 버전 이력 조회')
62
+ .action(async () => {
63
+ const globalOpts = program.opts();
64
+ try {
65
+ const client = createClient({ projectId: globalOpts.project });
66
+ const { data } = await client.get('/prd/versions');
67
+ formatOutput(data, globalOpts);
68
+ } catch (error) {
69
+ // Error already handled
70
+ }
71
+ });
72
+ }