@jcjeon/integration-cli 0.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.
Files changed (264) hide show
  1. package/.gitignore +23 -0
  2. package/.npmignore +21 -0
  3. package/.prettierignore +6 -0
  4. package/.prettierrc +26 -0
  5. package/AGENTS.md +10 -0
  6. package/CLAUDE.md +10 -0
  7. package/README.md +384 -0
  8. package/apps/server/README.md +294 -0
  9. package/apps/server/eslint.config.mjs +20 -0
  10. package/apps/server/nest-cli.json +8 -0
  11. package/apps/server/package.json +89 -0
  12. package/apps/server/scripts/postinstall.js +53 -0
  13. package/apps/server/src/__mocks__/glob.js +6 -0
  14. package/apps/server/src/__mocks__/uuid.js +5 -0
  15. package/apps/server/src/app.controller.spec.ts +24 -0
  16. package/apps/server/src/app.controller.ts +13 -0
  17. package/apps/server/src/app.module.ts +18 -0
  18. package/apps/server/src/app.service.ts +8 -0
  19. package/apps/server/src/common/ji-paths.ts +41 -0
  20. package/apps/server/src/database/database.module.ts +27 -0
  21. package/apps/server/src/database/entities/agent-changelog.entity.ts +39 -0
  22. package/apps/server/src/database/entities/agent-session.entity.ts +29 -0
  23. package/apps/server/src/database/entities/conversation.entity.ts +41 -0
  24. package/apps/server/src/database/entities/session.entity.ts +16 -0
  25. package/apps/server/src/database/entities/task-agent-run.entity.ts +40 -0
  26. package/apps/server/src/database/entities/task-agent.entity.ts +42 -0
  27. package/apps/server/src/database/entities/task-requirement.entity.ts +27 -0
  28. package/apps/server/src/database/entities/task-run.entity.ts +41 -0
  29. package/apps/server/src/database/entities/task.entity.ts +44 -0
  30. package/apps/server/src/main.ts +65 -0
  31. package/apps/server/src/modules/agents/agent-model-settings.spec.ts +80 -0
  32. package/apps/server/src/modules/agents/agents.module.ts +11 -0
  33. package/apps/server/src/modules/agents/claude/claude-auth.manager.ts +83 -0
  34. package/apps/server/src/modules/agents/claude/claude-pty.manager.ts +380 -0
  35. package/apps/server/src/modules/agents/claude/claude.controller.ts +85 -0
  36. package/apps/server/src/modules/agents/claude/claude.gateway.ts +158 -0
  37. package/apps/server/src/modules/agents/claude/claude.module.ts +18 -0
  38. package/apps/server/src/modules/agents/claude/claude.service.ts +67 -0
  39. package/apps/server/src/modules/agents/claude/dto/create-session.dto.ts +24 -0
  40. package/apps/server/src/modules/agents/claude/dto/resize-session.dto.ts +13 -0
  41. package/apps/server/src/modules/agents/claude/dto/send-input.dto.ts +9 -0
  42. package/apps/server/src/modules/agents/claude/interfaces/claude-session.interface.ts +26 -0
  43. package/apps/server/src/modules/agents/claude/interfaces/pty-event.interface.ts +10 -0
  44. package/apps/server/src/modules/agents/claude/interfaces/stream-event.interface.ts +61 -0
  45. package/apps/server/src/modules/agents/codex/codex-auth.manager.ts +107 -0
  46. package/apps/server/src/modules/agents/codex/codex-session.manager.ts +357 -0
  47. package/apps/server/src/modules/agents/codex/codex.controller.ts +64 -0
  48. package/apps/server/src/modules/agents/codex/codex.gateway.ts +97 -0
  49. package/apps/server/src/modules/agents/codex/codex.module.ts +17 -0
  50. package/apps/server/src/modules/agents/codex/dto/configure-auth.dto.ts +7 -0
  51. package/apps/server/src/modules/agents/gemini/dto/configure-auth.dto.ts +15 -0
  52. package/apps/server/src/modules/agents/gemini/dto/create-session.dto.ts +9 -0
  53. package/apps/server/src/modules/agents/gemini/dto/send-input.dto.ts +9 -0
  54. package/apps/server/src/modules/agents/gemini/gemini-auth.manager.ts +157 -0
  55. package/apps/server/src/modules/agents/gemini/gemini-session.manager.ts +287 -0
  56. package/apps/server/src/modules/agents/gemini/gemini.controller.ts +93 -0
  57. package/apps/server/src/modules/agents/gemini/gemini.gateway.ts +149 -0
  58. package/apps/server/src/modules/agents/gemini/gemini.module.ts +17 -0
  59. package/apps/server/src/modules/agents/gemini/interfaces/gemini-session.interface.ts +18 -0
  60. package/apps/server/src/modules/agents/gemini/interfaces/stream-event.interface.ts +14 -0
  61. package/apps/server/src/modules/agents/session-termination.spec.ts +103 -0
  62. package/apps/server/src/modules/changelog/changelog.controller.ts +20 -0
  63. package/apps/server/src/modules/changelog/changelog.module.ts +14 -0
  64. package/apps/server/src/modules/changelog/changelog.service.spec.ts +531 -0
  65. package/apps/server/src/modules/changelog/changelog.service.ts +690 -0
  66. package/apps/server/src/modules/conversations/conversation.controller.spec.ts +106 -0
  67. package/apps/server/src/modules/conversations/conversation.controller.ts +60 -0
  68. package/apps/server/src/modules/conversations/conversation.module.ts +14 -0
  69. package/apps/server/src/modules/conversations/conversation.service.spec.ts +176 -0
  70. package/apps/server/src/modules/conversations/conversation.service.ts +54 -0
  71. package/apps/server/src/modules/conversations/dto/create-conversation.dto.ts +37 -0
  72. package/apps/server/src/modules/conversations/enums/conversation.enum.ts +13 -0
  73. package/apps/server/src/modules/fs/fs.controller.ts +29 -0
  74. package/apps/server/src/modules/fs/fs.module.ts +8 -0
  75. package/apps/server/src/modules/harness/dto/save-harness.dto.ts +9 -0
  76. package/apps/server/src/modules/harness/harness.controller.spec.ts +95 -0
  77. package/apps/server/src/modules/harness/harness.controller.ts +35 -0
  78. package/apps/server/src/modules/harness/harness.module.ts +11 -0
  79. package/apps/server/src/modules/harness/harness.service.spec.ts +217 -0
  80. package/apps/server/src/modules/harness/harness.service.ts +112 -0
  81. package/apps/server/src/modules/sessions/session.controller.spec.ts +68 -0
  82. package/apps/server/src/modules/sessions/session.controller.ts +43 -0
  83. package/apps/server/src/modules/sessions/session.module.ts +14 -0
  84. package/apps/server/src/modules/sessions/session.service.spec.ts +106 -0
  85. package/apps/server/src/modules/sessions/session.service.ts +35 -0
  86. package/apps/server/src/modules/tasks/dto/create-task.dto.ts +54 -0
  87. package/apps/server/src/modules/tasks/dto/execute-task.dto.ts +22 -0
  88. package/apps/server/src/modules/tasks/dto/merge-file.dto.ts +7 -0
  89. package/apps/server/src/modules/tasks/dto/rerun-task.dto.ts +14 -0
  90. package/apps/server/src/modules/tasks/dto/update-task.dto.ts +55 -0
  91. package/apps/server/src/modules/tasks/task-execution.service.ts +978 -0
  92. package/apps/server/src/modules/tasks/task.gateway.ts +140 -0
  93. package/apps/server/src/modules/tasks/tasks.controller.spec.ts +210 -0
  94. package/apps/server/src/modules/tasks/tasks.controller.ts +139 -0
  95. package/apps/server/src/modules/tasks/tasks.module.ts +30 -0
  96. package/apps/server/src/modules/tasks/tasks.service.spec.ts +552 -0
  97. package/apps/server/src/modules/tasks/tasks.service.ts +333 -0
  98. package/apps/server/test/app.e2e-spec.ts +28 -0
  99. package/apps/server/test/jest-e2e.json +9 -0
  100. package/apps/server/tsconfig.build.json +4 -0
  101. package/apps/server/tsconfig.json +13 -0
  102. package/apps/web/AGENTS.md +7 -0
  103. package/apps/web/CLAUDE.md +1 -0
  104. package/apps/web/README.md +36 -0
  105. package/apps/web/eslint.config.mjs +21 -0
  106. package/apps/web/next-env.d.ts +6 -0
  107. package/apps/web/next.config.ts +7 -0
  108. package/apps/web/package.json +49 -0
  109. package/apps/web/postcss.config.mjs +7 -0
  110. package/apps/web/public/file.svg +1 -0
  111. package/apps/web/public/globe.svg +1 -0
  112. package/apps/web/public/next.svg +1 -0
  113. package/apps/web/public/vercel.svg +1 -0
  114. package/apps/web/public/window.svg +1 -0
  115. package/apps/web/src/app/claude/page.tsx +5 -0
  116. package/apps/web/src/app/codex/page.tsx +126 -0
  117. package/apps/web/src/app/favicon.ico +0 -0
  118. package/apps/web/src/app/gemini/page.tsx +130 -0
  119. package/apps/web/src/app/globals.css +149 -0
  120. package/apps/web/src/app/layout.tsx +40 -0
  121. package/apps/web/src/app/login/page.tsx +67 -0
  122. package/apps/web/src/app/page.tsx +497 -0
  123. package/apps/web/src/app/task/[id]/page.tsx +11 -0
  124. package/apps/web/src/app/test/page.tsx +298 -0
  125. package/apps/web/src/components/ui/Modal.tsx +78 -0
  126. package/apps/web/src/components/ui/WorkingDirPicker.tsx +195 -0
  127. package/apps/web/src/components/ui/__tests__/Modal.test.tsx +68 -0
  128. package/apps/web/src/features/auth/api/__tests__/auth.api.test.ts +83 -0
  129. package/apps/web/src/features/auth/api/auth.api.ts +81 -0
  130. package/apps/web/src/features/auth/hooks/__tests__/useClaudeAuth.test.ts +166 -0
  131. package/apps/web/src/features/auth/hooks/__tests__/useCodexAuth.test.ts +127 -0
  132. package/apps/web/src/features/auth/hooks/__tests__/useGeminiAuth.test.ts +120 -0
  133. package/apps/web/src/features/auth/hooks/useClaudeAuth.ts +88 -0
  134. package/apps/web/src/features/auth/hooks/useCodexAuth.ts +149 -0
  135. package/apps/web/src/features/auth/hooks/useGeminiAuth.ts +125 -0
  136. package/apps/web/src/features/auth/ui/CodexLoginPanel.tsx +302 -0
  137. package/apps/web/src/features/auth/ui/GeminiLoginPanel.tsx +316 -0
  138. package/apps/web/src/features/auth/ui/LoginForm.tsx +190 -0
  139. package/apps/web/src/features/auth/ui/LoginPanel.tsx +114 -0
  140. package/apps/web/src/features/auth/ui/__tests__/LoginPanel.test.tsx +105 -0
  141. package/apps/web/src/features/chat/api/__tests__/sessions.api.test.ts +187 -0
  142. package/apps/web/src/features/chat/api/sessions.api.ts +161 -0
  143. package/apps/web/src/features/chat/container/ClaudePageContainer.tsx +152 -0
  144. package/apps/web/src/features/chat/hooks/__tests__/useCodexSessions.test.ts +131 -0
  145. package/apps/web/src/features/chat/hooks/__tests__/useGeminiSessions.test.ts +130 -0
  146. package/apps/web/src/features/chat/hooks/useAgentModelSettings.ts +54 -0
  147. package/apps/web/src/features/chat/hooks/useClaudeSessions.ts +323 -0
  148. package/apps/web/src/features/chat/hooks/useCodexSessions.ts +275 -0
  149. package/apps/web/src/features/chat/hooks/useGeminiSessions.ts +255 -0
  150. package/apps/web/src/features/chat/hooks/useSessionCommand.ts +66 -0
  151. package/apps/web/src/features/chat/hooks/useSessionRename.ts +61 -0
  152. package/apps/web/src/features/chat/hooks/useSessionWorkingDirectories.ts +34 -0
  153. package/apps/web/src/features/chat/hooks/useUnifiedSessions.ts +156 -0
  154. package/apps/web/src/features/chat/lib/agentModelOptions.ts +72 -0
  155. package/apps/web/src/features/chat/ui/AgentModelPicker.tsx +134 -0
  156. package/apps/web/src/features/chat/ui/AgentSelectModal.tsx +236 -0
  157. package/apps/web/src/features/chat/ui/ChatInput.tsx +162 -0
  158. package/apps/web/src/features/chat/ui/ChatMessage.tsx +204 -0
  159. package/apps/web/src/features/chat/ui/ChatWorkspace.tsx +207 -0
  160. package/apps/web/src/features/chat/ui/CheckingSkeleton.tsx +44 -0
  161. package/apps/web/src/features/chat/ui/ClaudeLoginView.tsx +44 -0
  162. package/apps/web/src/features/chat/ui/PermissionCard.tsx +37 -0
  163. package/apps/web/src/features/chat/ui/SessionSidebar.tsx +280 -0
  164. package/apps/web/src/features/chat/ui/__tests__/AgentSelectModal.test.tsx +58 -0
  165. package/apps/web/src/features/chat/ui/__tests__/ChatInput.test.tsx +134 -0
  166. package/apps/web/src/features/chat/ui/__tests__/ChatMessage.test.tsx +106 -0
  167. package/apps/web/src/features/chat/ui/__tests__/ChatWorkspace.test.tsx +66 -0
  168. package/apps/web/src/features/diff/ui/DiffFileRow.tsx +73 -0
  169. package/apps/web/src/features/diff/ui/DiffHunk.tsx +61 -0
  170. package/apps/web/src/features/diff/ui/FileChangeBadge.tsx +23 -0
  171. package/apps/web/src/features/diff/ui/__tests__/DiffFileRow.test.tsx +40 -0
  172. package/apps/web/src/features/diff/ui/__tests__/DiffHunk.test.tsx +24 -0
  173. package/apps/web/src/features/diff/ui/__tests__/FileChangeBadge.test.tsx +16 -0
  174. package/apps/web/src/features/fs/api/fs.api.ts +14 -0
  175. package/apps/web/src/features/fs/hooks/useDirBrowser.ts +50 -0
  176. package/apps/web/src/features/harness/api/__tests__/harness.api.test.ts +73 -0
  177. package/apps/web/src/features/harness/api/harness.api.ts +46 -0
  178. package/apps/web/src/features/harness/hooks/__tests__/useHarness.test.ts +65 -0
  179. package/apps/web/src/features/harness/hooks/useHarness.ts +66 -0
  180. package/apps/web/src/features/harness/ui/HarnessModal.tsx +171 -0
  181. package/apps/web/src/features/harness/ui/__tests__/HarnessModal.test.tsx +46 -0
  182. package/apps/web/src/features/status/ui/AgentStatusModal.tsx +267 -0
  183. package/apps/web/src/features/status/ui/__tests__/AgentStatusModal.test.tsx +71 -0
  184. package/apps/web/src/features/tasks/api/__tests__/changelog.api.test.ts +89 -0
  185. package/apps/web/src/features/tasks/api/__tests__/tasks.api.test.ts +282 -0
  186. package/apps/web/src/features/tasks/api/changelog.api.ts +52 -0
  187. package/apps/web/src/features/tasks/api/tasks.api.ts +175 -0
  188. package/apps/web/src/features/tasks/container/TaskDetailPageContainer.tsx +69 -0
  189. package/apps/web/src/features/tasks/hooks/__tests__/useChangelogCodeCopy.test.ts +48 -0
  190. package/apps/web/src/features/tasks/hooks/__tests__/useTaskChangelog.test.ts +48 -0
  191. package/apps/web/src/features/tasks/hooks/__tests__/useTaskCreate.test.ts +217 -0
  192. package/apps/web/src/features/tasks/hooks/__tests__/useTaskEdit.test.ts +152 -0
  193. package/apps/web/src/features/tasks/hooks/__tests__/useTaskExecution.test.ts +143 -0
  194. package/apps/web/src/features/tasks/hooks/__tests__/useTaskList.test.ts +168 -0
  195. package/apps/web/src/features/tasks/hooks/__tests__/useTaskNotification.test.ts +125 -0
  196. package/apps/web/src/features/tasks/hooks/__tests__/useTaskRuns.test.ts +51 -0
  197. package/apps/web/src/features/tasks/hooks/useChangelogCodeCopy.ts +52 -0
  198. package/apps/web/src/features/tasks/hooks/useCopyToClipboard.ts +47 -0
  199. package/apps/web/src/features/tasks/hooks/useTaskChangelog.ts +32 -0
  200. package/apps/web/src/features/tasks/hooks/useTaskCreate.ts +137 -0
  201. package/apps/web/src/features/tasks/hooks/useTaskDetail.ts +217 -0
  202. package/apps/web/src/features/tasks/hooks/useTaskEdit.ts +130 -0
  203. package/apps/web/src/features/tasks/hooks/useTaskExecution.ts +137 -0
  204. package/apps/web/src/features/tasks/hooks/useTaskList.ts +159 -0
  205. package/apps/web/src/features/tasks/hooks/useTaskNotification.ts +80 -0
  206. package/apps/web/src/features/tasks/hooks/useTaskRuns.ts +32 -0
  207. package/apps/web/src/features/tasks/ui/AgentOutputPanel.tsx +203 -0
  208. package/apps/web/src/features/tasks/ui/AgentRoleSelect.tsx +97 -0
  209. package/apps/web/src/features/tasks/ui/ChangelogPanel.tsx +321 -0
  210. package/apps/web/src/features/tasks/ui/RunHistoryPanel.tsx +193 -0
  211. package/apps/web/src/features/tasks/ui/TaskCreateModal.tsx +205 -0
  212. package/apps/web/src/features/tasks/ui/TaskDetailView.tsx +413 -0
  213. package/apps/web/src/features/tasks/ui/TaskEditModal.tsx +165 -0
  214. package/apps/web/src/features/tasks/ui/TaskListModal.tsx +591 -0
  215. package/apps/web/src/features/tasks/ui/__tests__/AgentRoleSelect.test.tsx +91 -0
  216. package/apps/web/src/features/tasks/ui/__tests__/ChangelogPanel.test.tsx +94 -0
  217. package/apps/web/src/features/tasks/ui/__tests__/RunHistoryPanel.test.tsx +71 -0
  218. package/apps/web/src/features/tasks/ui/__tests__/TaskCreateModal.test.tsx +153 -0
  219. package/apps/web/src/features/tasks/ui/__tests__/TaskEditModal.test.tsx +75 -0
  220. package/apps/web/src/features/tasks/ui/__tests__/TaskListModal.test.tsx +243 -0
  221. package/apps/web/src/hooks/useWorkingDir.ts +28 -0
  222. package/apps/web/src/lib/__tests__/ansi.test.ts +88 -0
  223. package/apps/web/src/lib/ansi.ts +105 -0
  224. package/apps/web/src/lib/constants.ts +4 -0
  225. package/apps/web/src/lib/quota.ts +22 -0
  226. package/apps/web/src/lib/theme.tsx +78 -0
  227. package/apps/web/src/lib/toast.tsx +175 -0
  228. package/apps/web/src/store/agentStatusStore.ts +38 -0
  229. package/apps/web/tsconfig.json +18 -0
  230. package/apps/web/vitest.config.ts +25 -0
  231. package/apps/web/vitest.setup.ts +10 -0
  232. package/package.json +85 -0
  233. package/packages/cli/dist/commands/check.d.ts +1 -0
  234. package/packages/cli/dist/commands/check.js +89 -0
  235. package/packages/cli/dist/commands/init.d.ts +5 -0
  236. package/packages/cli/dist/commands/init.js +183 -0
  237. package/packages/cli/dist/commands/start.d.ts +4 -0
  238. package/packages/cli/dist/commands/start.js +188 -0
  239. package/packages/cli/dist/index.d.ts +2 -0
  240. package/packages/cli/dist/index.js +71 -0
  241. package/packages/cli/dist/utils/agent-tools.d.ts +28 -0
  242. package/packages/cli/dist/utils/agent-tools.js +193 -0
  243. package/packages/cli/dist/utils/project-init.d.ts +12 -0
  244. package/packages/cli/dist/utils/project-init.js +258 -0
  245. package/packages/cli/dist/utils/proxy.d.ts +8 -0
  246. package/packages/cli/dist/utils/proxy.js +138 -0
  247. package/packages/cli/package.json +30 -0
  248. package/packages/cli/src/commands/check.ts +77 -0
  249. package/packages/cli/src/commands/init.ts +209 -0
  250. package/packages/cli/src/commands/start.ts +183 -0
  251. package/packages/cli/src/index.ts +91 -0
  252. package/packages/cli/src/utils/agent-tools.ts +201 -0
  253. package/packages/cli/src/utils/project-init.ts +252 -0
  254. package/packages/cli/src/utils/proxy.ts +123 -0
  255. package/packages/cli/tsconfig.json +14 -0
  256. package/packages/eslint-config/base.mjs +31 -0
  257. package/packages/eslint-config/nest.mjs +55 -0
  258. package/packages/eslint-config/next.mjs +23 -0
  259. package/packages/eslint-config/package.json +20 -0
  260. package/packages/typescript-config/base.json +16 -0
  261. package/packages/typescript-config/nestjs.json +17 -0
  262. package/packages/typescript-config/nextjs.json +15 -0
  263. package/packages/typescript-config/package.json +11 -0
  264. package/turbo.json +28 -0
@@ -0,0 +1,209 @@
1
+ import * as os from 'os';
2
+
3
+ import chalk from 'chalk';
4
+ import ora from 'ora';
5
+
6
+ import {
7
+ ensureProjectReady,
8
+ type InitProjectOptions,
9
+ } from '../utils/project-init';
10
+ import {
11
+ AGENT_TOOLS,
12
+ MIN_NODE_MAJOR,
13
+ findWindowsGitBashPath,
14
+ getCommandVersion,
15
+ getNodeMajorVersion,
16
+ getNpmCommand,
17
+ getPlatform,
18
+ installGlobalPackage,
19
+ isCommandAvailable,
20
+ type AgentTool,
21
+ type SupportedPlatform,
22
+ } from '../utils/agent-tools';
23
+
24
+ export interface RunInitOptions extends InitProjectOptions {
25
+ skipAgents?: boolean;
26
+ }
27
+
28
+ export async function runInit(options: RunInitOptions = {}): Promise<void> {
29
+ const platform = getPlatform();
30
+ const failures: string[] = [];
31
+
32
+ console.log(chalk.bold('\n🚀 jccli init - 프로젝트 초기화\n'));
33
+ console.log(chalk.gray(`플랫폼: ${platform} (${os.arch()})`));
34
+ console.log(chalk.gray(`Node.js: ${process.version}\n`));
35
+
36
+ if (!validateNodeVersion()) {
37
+ process.exitCode = 1;
38
+ return;
39
+ }
40
+
41
+ if (!validateNpm(platform)) {
42
+ process.exitCode = 1;
43
+ return;
44
+ }
45
+
46
+ const projectReady = ensureProjectReady(options, platform);
47
+ if (!projectReady.ok) {
48
+ process.exitCode = 1;
49
+ return;
50
+ }
51
+
52
+ if (options.skipAgents) {
53
+ printProjectOnlySummary(projectReady.projectRoot);
54
+ return;
55
+ }
56
+
57
+ printWindowsClaudeNotice(platform);
58
+
59
+ for (const tool of AGENT_TOOLS) {
60
+ const ok = ensureToolInstalled(tool, platform);
61
+ if (!ok) failures.push(tool.name);
62
+ }
63
+
64
+ printSummary(failures, platform, projectReady.projectRoot);
65
+
66
+ if (failures.length > 0) {
67
+ process.exitCode = 1;
68
+ }
69
+ }
70
+
71
+ function validateNodeVersion(): boolean {
72
+ const nodeMajor = getNodeMajorVersion();
73
+ if (nodeMajor >= MIN_NODE_MAJOR) {
74
+ console.log(chalk.green(`✓ Node.js ${process.version}`));
75
+ return true;
76
+ }
77
+
78
+ console.error(
79
+ chalk.red(`✗ Node.js v${MIN_NODE_MAJOR}+ 이상이 필요합니다. 현재: ${process.version}`),
80
+ );
81
+ console.error(chalk.yellow(' https://nodejs.org 에서 최신 Node.js를 설치해 주세요.'));
82
+ return false;
83
+ }
84
+
85
+ function validateNpm(platform: SupportedPlatform): boolean {
86
+ if (!isCommandAvailable('npm', platform)) {
87
+ console.error(chalk.red('✗ npm이 설치되어 있지 않습니다.'));
88
+ console.error(chalk.yellow(' Node.js 재설치 또는 npm을 먼저 설치해 주세요.'));
89
+ return false;
90
+ }
91
+
92
+ const npmVersion = getCommandVersion(getNpmCommand(platform), platform) ?? 'unknown';
93
+ console.log(chalk.green(`✓ npm v${npmVersion}\n`));
94
+ return true;
95
+ }
96
+
97
+ function printWindowsClaudeNotice(platform: SupportedPlatform): void {
98
+ if (platform !== 'windows') return;
99
+
100
+ const gitBashPath = findWindowsGitBashPath();
101
+ if (gitBashPath) {
102
+ console.log(chalk.gray(`Claude Code Git Bash 감지: ${gitBashPath}\n`));
103
+ return;
104
+ }
105
+
106
+ console.log(chalk.yellow('Claude Code는 Windows 네이티브 실행 시 Git for Windows가 필요할 수 있습니다.'));
107
+ console.log(chalk.gray(' 설치 후에도 claude 실행이 실패하면 Git for Windows를 설치하거나'));
108
+ console.log(chalk.gray(' CLAUDE_CODE_GIT_BASH_PATH 환경 변수를 bash.exe 경로로 설정해 주세요.\n'));
109
+ }
110
+
111
+ function ensureToolInstalled(
112
+ tool: AgentTool,
113
+ platform: SupportedPlatform,
114
+ ): boolean {
115
+ console.log(chalk.bold(`[ ${tool.name} ]`));
116
+
117
+ if (isCommandAvailable(tool.command, platform)) {
118
+ const version = getCommandVersion(tool.command, platform) ?? 'version unknown';
119
+ console.log(chalk.green(`✓ 이미 설치됨: ${version}\n`));
120
+ return true;
121
+ }
122
+
123
+ console.log(chalk.yellow(` 미설치 상태입니다. ${tool.packageName} 설치를 시작합니다.`));
124
+
125
+ const spinner = ora({
126
+ text: `${tool.packageName} 전역 설치 중...`,
127
+ color: 'cyan',
128
+ }).start();
129
+ const result = installGlobalPackage(tool.packageName, platform);
130
+
131
+ if (!result.ok) {
132
+ spinner.fail('설치 실패');
133
+ printInstallError(result.stderr, result.error, tool, platform);
134
+ return false;
135
+ }
136
+
137
+ spinner.succeed('설치 완료');
138
+
139
+ const verifySpinner = ora('설치 확인 중...').start();
140
+ if (!isCommandAvailable(tool.command, platform)) {
141
+ verifySpinner.fail(`${tool.command} 명령을 찾을 수 없습니다.`);
142
+ console.log(chalk.yellow(' 새 터미널을 열거나 PATH 설정을 확인해 주세요.\n'));
143
+ return false;
144
+ }
145
+
146
+ const version = getCommandVersion(tool.command, platform) ?? 'version unknown';
147
+ verifySpinner.succeed(`${tool.name} 설치 확인: ${version}`);
148
+ console.log();
149
+ return true;
150
+ }
151
+
152
+ function printInstallError(
153
+ stderr: string,
154
+ error: string | undefined,
155
+ tool: AgentTool,
156
+ platform: SupportedPlatform,
157
+ ): void {
158
+ console.error(chalk.red('\n설치 중 오류가 발생했습니다:'));
159
+ if (stderr) console.error(chalk.red(stderr));
160
+ if (error) console.error(chalk.red(error));
161
+
162
+ console.error(chalk.yellow('\n수동 설치 명령:'));
163
+ console.error(chalk.gray(` npm install -g ${tool.packageName}`));
164
+
165
+ if (platform === 'windows' && tool.requiresWindowsGitBash) {
166
+ console.error(chalk.gray(' Claude Code 실행에는 Git for Windows 또는 WSL이 필요할 수 있습니다.'));
167
+ }
168
+
169
+ if (platform !== 'windows') {
170
+ console.error(chalk.gray(' 권한 오류가 나면 npm global prefix 또는 Node 설치 방식을 확인해 주세요.'));
171
+ }
172
+
173
+ console.log();
174
+ }
175
+
176
+ function printProjectOnlySummary(projectRoot: string): void {
177
+ console.log(chalk.bold.green('프로젝트 초기화가 완료되었습니다.\n'));
178
+ console.log(chalk.cyan('다음 명령으로 실행할 수 있습니다:'));
179
+ console.log(chalk.gray(` cd ${projectRoot}`));
180
+ console.log(chalk.gray(' jccli start # http://localhost:3020\n'));
181
+ }
182
+
183
+ function printSummary(
184
+ failures: string[],
185
+ platform: SupportedPlatform,
186
+ projectRoot: string,
187
+ ): void {
188
+ if (failures.length === 0) {
189
+ console.log(chalk.bold.green('모든 에이전트 CLI 설치 확인이 완료되었습니다.\n'));
190
+ console.log(chalk.cyan('다음 명령으로 시작할 수 있습니다:'));
191
+ console.log(chalk.gray(` cd ${projectRoot}`));
192
+ console.log(chalk.gray(' jccli start # http://localhost:3020'));
193
+ console.log();
194
+ console.log(chalk.cyan('에이전트 CLI 직접 실행:'));
195
+ console.log(chalk.gray(' claude # Claude Code'));
196
+ console.log(chalk.gray(' gemini # Gemini CLI'));
197
+ console.log(chalk.gray(' codex # OpenAI Codex\n'));
198
+
199
+ if (platform !== 'windows') {
200
+ console.log(chalk.yellow('명령을 찾을 수 없으면 터미널을 다시 열거나 셸 설정을 다시 로드해 주세요.'));
201
+ console.log(chalk.gray(' source ~/.zshrc # 또는 ~/.bashrc\n'));
202
+ }
203
+ return;
204
+ }
205
+
206
+ console.log(chalk.bold.yellow('일부 에이전트 CLI 설치 또는 확인에 실패했습니다.'));
207
+ console.log(chalk.yellow(`실패 항목: ${failures.join(', ')}`));
208
+ console.log(chalk.gray('위 안내에 따라 수동 설치 또는 PATH 설정을 확인해 주세요.\n'));
209
+ }
@@ -0,0 +1,183 @@
1
+ import { existsSync } from 'fs';
2
+ import * as path from 'path';
3
+ import { spawn, type ChildProcess } from 'child_process';
4
+
5
+ import chalk from 'chalk';
6
+ import ora from 'ora';
7
+
8
+ import { findProjectRoot } from '../utils/project-init';
9
+ import { getNpmCommand, getPlatform, type SupportedPlatform } from '../utils/agent-tools';
10
+ import { startProxyServer, waitForPort } from '../utils/proxy';
11
+
12
+ export interface RunStartOptions {
13
+ port?: string;
14
+ }
15
+
16
+ const DEFAULT_PORT = 3020;
17
+ const UPSTREAM_READY_TIMEOUT_MS = 180_000;
18
+
19
+ export async function runStart(options: RunStartOptions = {}): Promise<void> {
20
+ const platform = getPlatform();
21
+
22
+ const port = parsePort(options.port);
23
+ if (port === null) {
24
+ console.error(chalk.red(`✗ 잘못된 포트입니다: ${options.port}`));
25
+ process.exitCode = 1;
26
+ return;
27
+ }
28
+
29
+ const projectRoot = findProjectRoot(process.cwd());
30
+ if (!projectRoot) {
31
+ console.error(chalk.red('✗ jccli 프로젝트를 찾을 수 없습니다.'));
32
+ console.error(chalk.gray(' 먼저 jccli init 으로 프로젝트를 초기화한 뒤, 프로젝트 폴더 안에서 실행해 주세요.'));
33
+ process.exitCode = 1;
34
+ return;
35
+ }
36
+
37
+ if (!existsSync(path.join(projectRoot, 'node_modules'))) {
38
+ console.error(chalk.red('✗ 프로젝트 의존성이 설치되어 있지 않습니다.'));
39
+ console.error(chalk.gray(` cd ${projectRoot} && jccli init 을 먼저 실행해 주세요.`));
40
+ process.exitCode = 1;
41
+ return;
42
+ }
43
+
44
+ const serverPort = port + 1;
45
+ const webPort = port + 2;
46
+
47
+ console.log(chalk.bold('\n🚀 jccli start\n'));
48
+ console.log(chalk.gray(`프로젝트: ${projectRoot}`));
49
+ console.log(chalk.gray(`포트: ${port} (내부: server ${serverPort}, web ${webPort})\n`));
50
+
51
+ const children: ChildProcess[] = [];
52
+ let shuttingDown = false;
53
+
54
+ const shutdown = (code: number): void => {
55
+ if (shuttingDown) return;
56
+ shuttingDown = true;
57
+ for (const child of children) {
58
+ try {
59
+ child.kill();
60
+ } catch {
61
+ // 이미 종료된 프로세스는 무시
62
+ }
63
+ }
64
+ process.exit(code);
65
+ };
66
+
67
+ process.on('SIGINT', () => shutdown(0));
68
+ process.on('SIGTERM', () => shutdown(0));
69
+
70
+ try {
71
+ const proxyServer = await startProxyServer({ port, serverPort, webPort });
72
+ proxyServer.on('error', (error) => {
73
+ console.error(chalk.red(`프록시 서버 오류: ${error.message}`));
74
+ });
75
+ } catch (error) {
76
+ console.error(chalk.red(`✗ 포트 ${port} 를 사용할 수 없습니다. 이미 사용 중인지 확인해 주세요.`));
77
+ console.error(chalk.red(error instanceof Error ? error.message : String(error)));
78
+ process.exitCode = 1;
79
+ return;
80
+ }
81
+
82
+ const serverChild = spawnWorkspace({
83
+ label: 'server',
84
+ color: chalk.magenta,
85
+ npmArgs: ['run', 'dev', '--workspace', '@ji/server'],
86
+ env: { PORT: String(serverPort) },
87
+ projectRoot,
88
+ platform,
89
+ onExit: (code) => {
90
+ if (!shuttingDown) {
91
+ console.error(chalk.red(`\n[server] 프로세스가 종료되었습니다 (code: ${code ?? 'unknown'})`));
92
+ shutdown(1);
93
+ }
94
+ },
95
+ });
96
+ children.push(serverChild);
97
+
98
+ const webChild = spawnWorkspace({
99
+ label: 'web',
100
+ color: chalk.cyan,
101
+ npmArgs: ['run', 'dev', '--workspace', '@ji/web', '--', '--port', String(webPort)],
102
+ env: { NEXT_PUBLIC_SERVER_URL: `http://localhost:${port}` },
103
+ projectRoot,
104
+ platform,
105
+ onExit: (code) => {
106
+ if (!shuttingDown) {
107
+ console.error(chalk.red(`\n[web] 프로세스가 종료되었습니다 (code: ${code ?? 'unknown'})`));
108
+ shutdown(1);
109
+ }
110
+ },
111
+ });
112
+ children.push(webChild);
113
+
114
+ const spinner = ora({ text: '서버와 웹 앱을 시작하는 중...', color: 'cyan' }).start();
115
+
116
+ const [serverReady, webReady] = await Promise.all([
117
+ waitForPort(serverPort, UPSTREAM_READY_TIMEOUT_MS),
118
+ waitForPort(webPort, UPSTREAM_READY_TIMEOUT_MS),
119
+ ]);
120
+
121
+ if (!serverReady || !webReady) {
122
+ spinner.fail('시작 시간 초과');
123
+ const failed = [!serverReady && 'server', !webReady && 'web'].filter(Boolean).join(', ');
124
+ console.error(chalk.red(`✗ 다음 프로세스가 시간 안에 시작되지 않았습니다: ${failed}`));
125
+ console.error(chalk.gray(' 위에 출력된 로그를 확인해 주세요.'));
126
+ shutdown(1);
127
+ return;
128
+ }
129
+
130
+ spinner.succeed('모든 프로세스 시작 완료');
131
+ console.log();
132
+ console.log(chalk.bold.green(` ▶ http://localhost:${port}`));
133
+ console.log();
134
+ console.log(chalk.gray(' 종료하려면 Ctrl+C 를 누르세요.\n'));
135
+ }
136
+
137
+ interface SpawnWorkspaceOptions {
138
+ label: string;
139
+ color: (text: string) => string;
140
+ npmArgs: string[];
141
+ env: NodeJS.ProcessEnv;
142
+ projectRoot: string;
143
+ platform: SupportedPlatform;
144
+ onExit: (code: number | null) => void;
145
+ }
146
+
147
+ function spawnWorkspace(options: SpawnWorkspaceOptions): ChildProcess {
148
+ const { label, color, npmArgs, env, projectRoot, platform, onExit } = options;
149
+
150
+ const child = spawn(getNpmCommand(platform), npmArgs, {
151
+ cwd: projectRoot,
152
+ env: { ...process.env, ...env },
153
+ stdio: ['ignore', 'pipe', 'pipe'],
154
+ shell: platform === 'windows',
155
+ windowsHide: true,
156
+ });
157
+
158
+ const prefix = color(`[${label}]`);
159
+ const forward = (chunk: Buffer): void => {
160
+ for (const line of chunk.toString().split('\n')) {
161
+ if (line.trim().length === 0) continue;
162
+ console.log(`${prefix} ${line}`);
163
+ }
164
+ };
165
+
166
+ child.stdout?.on('data', forward);
167
+ child.stderr?.on('data', forward);
168
+ child.on('exit', onExit);
169
+ child.on('error', (error) => {
170
+ console.error(chalk.red(`[${label}] 실행 오류: ${error.message}`));
171
+ onExit(null);
172
+ });
173
+
174
+ return child;
175
+ }
176
+
177
+ function parsePort(value: string | undefined): number | null {
178
+ if (value === undefined) return DEFAULT_PORT;
179
+
180
+ const parsed = Number(value);
181
+ if (!Number.isInteger(parsed) || parsed < 1 || parsed > 65533) return null;
182
+ return parsed;
183
+ }
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+
4
+ import { runInit } from './commands/init';
5
+ import { runCheck } from './commands/check';
6
+ import { runStart } from './commands/start';
7
+
8
+ async function main(): Promise<void> {
9
+ const args = process.argv.slice(2);
10
+
11
+ // 단축 플래그 처리
12
+ if (args.includes('--init')) {
13
+ await runInit(parseInitArgs(args));
14
+ return;
15
+ }
16
+ if (args.includes('--check')) {
17
+ runCheck();
18
+ return;
19
+ }
20
+
21
+ const program = new Command();
22
+
23
+ program
24
+ .name('jccli')
25
+ .description('Claude Code, Gemini CLI, and Codex web integration CLI')
26
+ .version('0.2.0');
27
+
28
+ program
29
+ .command('init [dir]')
30
+ .description('프로젝트를 복사/초기화하고 Claude Code, Gemini CLI, Codex 설치 상태를 확인합니다')
31
+ .option('-f, --force', '비어 있지 않은 대상 폴더에도 프로젝트 파일을 복사합니다')
32
+ .option('--skip-install', '프로젝트 npm install을 건너뜁니다')
33
+ .option('--skip-agents', '에이전트 CLI 설치 확인을 건너뜁니다')
34
+ .action(async (dir: string | undefined, options: { force?: boolean; skipInstall?: boolean; skipAgents?: boolean }) => {
35
+ await runInit({
36
+ targetDir: dir,
37
+ force: options.force,
38
+ skipInstall: options.skipInstall,
39
+ skipAgents: options.skipAgents,
40
+ });
41
+ });
42
+
43
+ program
44
+ .command('start')
45
+ .description('서버와 웹 앱을 실행하고 단일 포트(기본 3020)에서 서비스합니다')
46
+ .option('-p, --port <port>', '서비스 포트', '3020')
47
+ .action(async (options: { port?: string }) => {
48
+ await runStart({ port: options.port });
49
+ });
50
+
51
+ program
52
+ .command('check')
53
+ .description('Claude Code, Gemini CLI, Codex 설치 상태를 확인합니다')
54
+ .action(() => {
55
+ runCheck();
56
+ });
57
+
58
+ program.addHelpText(
59
+ 'after',
60
+ '\nExamples:\n jccli --init\n jccli init\n jccli init my-app\n jccli init --skip-install\n jccli start\n jccli start --port 4000\n jccli check\n',
61
+ );
62
+
63
+ if (args.length === 0) {
64
+ program.outputHelp();
65
+ return;
66
+ }
67
+
68
+ await program.parseAsync(process.argv);
69
+ }
70
+
71
+ function parseInitArgs(args: string[]): {
72
+ targetDir?: string;
73
+ force?: boolean;
74
+ skipInstall?: boolean;
75
+ skipAgents?: boolean;
76
+ } {
77
+ const initIndex = args[0] === 'init' ? 1 : 0;
78
+ const positional = args.slice(initIndex).find((arg) => !arg.startsWith('-'));
79
+
80
+ return {
81
+ targetDir: positional,
82
+ force: args.includes('--force') || args.includes('-f'),
83
+ skipInstall: args.includes('--skip-install'),
84
+ skipAgents: args.includes('--skip-agents'),
85
+ };
86
+ }
87
+
88
+ main().catch((err: unknown) => {
89
+ console.error(err);
90
+ process.exit(1);
91
+ });
@@ -0,0 +1,201 @@
1
+ import { existsSync } from 'fs';
2
+ import * as os from 'os';
3
+ import { spawnSync } from 'child_process';
4
+
5
+ export type SupportedPlatform = 'mac' | 'windows' | 'linux';
6
+
7
+ export interface AgentTool {
8
+ name: string;
9
+ command: string;
10
+ packageName: string;
11
+ requiresWindowsGitBash?: boolean;
12
+ }
13
+
14
+ export interface CommandLookup {
15
+ command: string;
16
+ path: string;
17
+ }
18
+
19
+ export interface CommandResult {
20
+ ok: boolean;
21
+ stdout: string;
22
+ stderr: string;
23
+ error?: string;
24
+ }
25
+
26
+ export const MIN_NODE_MAJOR = 18;
27
+
28
+ export const AGENT_TOOLS: AgentTool[] = [
29
+ {
30
+ name: 'Claude Code',
31
+ command: 'claude',
32
+ packageName: '@anthropic-ai/claude-code',
33
+ requiresWindowsGitBash: true,
34
+ },
35
+ {
36
+ name: 'Gemini CLI',
37
+ command: 'gemini',
38
+ packageName: '@google/gemini-cli',
39
+ },
40
+ {
41
+ name: 'Codex',
42
+ command: 'codex',
43
+ packageName: '@openai/codex',
44
+ },
45
+ ];
46
+
47
+ export function getPlatform(): SupportedPlatform {
48
+ const platform = os.platform();
49
+ if (platform === 'darwin') return 'mac';
50
+ if (platform === 'win32') return 'windows';
51
+ return 'linux';
52
+ }
53
+
54
+ export function getNodeMajorVersion(): number {
55
+ return Number.parseInt(process.versions.node.split('.')[0] ?? '0', 10);
56
+ }
57
+
58
+ export function getNpmCommand(platform: SupportedPlatform): string {
59
+ return platform === 'windows' ? 'npm.cmd' : 'npm';
60
+ }
61
+
62
+ export function resolveCommand(
63
+ command: string,
64
+ platform: SupportedPlatform = getPlatform(),
65
+ ): CommandLookup | null {
66
+ try {
67
+ if (platform === 'windows') {
68
+ const candidates = command.match(/\.(cmd|exe|bat)$/i)
69
+ ? [command]
70
+ : [command, `${command}.cmd`, `${command}.exe`];
71
+
72
+ for (const candidate of candidates) {
73
+ const result = spawnSync('where.exe', [candidate], {
74
+ encoding: 'utf8',
75
+ windowsHide: true,
76
+ });
77
+ const path = result.stdout
78
+ ?.split(/\r?\n/)
79
+ .map((line) => line.trim())
80
+ .find(Boolean);
81
+
82
+ if (result.status === 0 && path) {
83
+ return { command, path };
84
+ }
85
+ }
86
+
87
+ return null;
88
+ }
89
+
90
+ const result = spawnSync('sh', ['-lc', `command -v ${quoteForPosixShell(command)}`], {
91
+ encoding: 'utf8',
92
+ });
93
+ const path = result.stdout.trim().split(/\r?\n/)[0];
94
+
95
+ if (result.status === 0 && path) {
96
+ return { command, path };
97
+ }
98
+
99
+ return null;
100
+ } catch {
101
+ return null;
102
+ }
103
+ }
104
+
105
+ export function isCommandAvailable(
106
+ command: string,
107
+ platform: SupportedPlatform = getPlatform(),
108
+ ): boolean {
109
+ return resolveCommand(command, platform) !== null;
110
+ }
111
+
112
+ export function runCommand(
113
+ command: string,
114
+ args: string[],
115
+ platform: SupportedPlatform = getPlatform(),
116
+ timeout = 5000,
117
+ ): CommandResult {
118
+ try {
119
+ const result = spawnSync(command, args, {
120
+ encoding: 'utf8',
121
+ shell: platform === 'windows',
122
+ timeout,
123
+ windowsHide: true,
124
+ });
125
+
126
+ return {
127
+ ok: result.status === 0 && !result.error,
128
+ stdout: result.stdout?.trim() ?? '',
129
+ stderr: result.stderr?.trim() ?? '',
130
+ error: result.error?.message,
131
+ };
132
+ } catch (error) {
133
+ return {
134
+ ok: false,
135
+ stdout: '',
136
+ stderr: '',
137
+ error: error instanceof Error ? error.message : 'Unknown command error',
138
+ };
139
+ }
140
+ }
141
+
142
+ export function getCommandVersion(
143
+ command: string,
144
+ platform: SupportedPlatform = getPlatform(),
145
+ ): string | null {
146
+ const result = runCommand(command, ['--version'], platform);
147
+ if (!result.ok) return null;
148
+
149
+ return [result.stdout, result.stderr].filter(Boolean).join('\n').trim() || null;
150
+ }
151
+
152
+ export function installGlobalPackage(
153
+ packageName: string,
154
+ platform: SupportedPlatform = getPlatform(),
155
+ ): CommandResult {
156
+ try {
157
+ const result = spawnSync(getNpmCommand(platform), ['install', '-g', packageName], {
158
+ stdio: ['inherit', 'pipe', 'pipe'],
159
+ encoding: 'utf8',
160
+ shell: platform === 'windows',
161
+ windowsHide: true,
162
+ });
163
+
164
+ return {
165
+ ok: result.status === 0 && !result.error,
166
+ stdout: result.stdout?.trim() ?? '',
167
+ stderr: result.stderr?.trim() ?? '',
168
+ error: result.error?.message,
169
+ };
170
+ } catch (error) {
171
+ return {
172
+ ok: false,
173
+ stdout: '',
174
+ stderr: '',
175
+ error: error instanceof Error ? error.message : 'Unknown npm install error',
176
+ };
177
+ }
178
+ }
179
+
180
+ export function findWindowsGitBashPath(): string | null {
181
+ if (getPlatform() !== 'windows') return null;
182
+
183
+ const fromPath = resolveCommand('bash', 'windows');
184
+ if (fromPath) return fromPath.path;
185
+
186
+ const candidates = [
187
+ process.env.CLAUDE_CODE_GIT_BASH_PATH,
188
+ 'C:\\Program Files\\Git\\bin\\bash.exe',
189
+ 'C:\\Program Files (x86)\\Git\\bin\\bash.exe',
190
+ ].filter((value): value is string => Boolean(value));
191
+
192
+ try {
193
+ return candidates.find((candidate) => existsSync(candidate)) ?? null;
194
+ } catch {
195
+ return null;
196
+ }
197
+ }
198
+
199
+ function quoteForPosixShell(value: string): string {
200
+ return `'${value.replace(/'/g, "'\\''")}'`;
201
+ }