sleepcode 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.
- package/.claude/settings.local.json +17 -0
- package/README.md +225 -178
- package/bin/index.js +503 -331
- package/package.json +17 -17
- package/templates/common/README.md +100 -97
- package/templates/common/ai_worker.ps1 +20 -0
- package/templates/common/ai_worker.sh +23 -23
- package/templates/common/log_filter.py +63 -63
- package/templates/common/run_forever.ps1 +59 -0
- package/templates/common/run_forever.sh +59 -59
- package/templates/rules/custom.md +69 -69
- package/templates/rules/nextjs.md +77 -77
- package/templates/rules/react-native.md +76 -76
- package/templates/rules/spring-boot.md +78 -78
- package/templates/settings/custom.json +19 -19
- package/templates/settings/nextjs.json +30 -30
- package/templates/settings/react-native.json +30 -30
- package/templates/settings/spring-boot.json +24 -24
package/bin/index.js
CHANGED
|
@@ -1,331 +1,503 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
const readline = require('readline');
|
|
4
|
-
const fs = require('fs');
|
|
5
|
-
const path = require('path');
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
//
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
console.
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const readline = require('readline');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { execSync } = require('child_process');
|
|
7
|
+
|
|
8
|
+
// ─── 색상 ───
|
|
9
|
+
const C = {
|
|
10
|
+
reset: '\x1b[0m',
|
|
11
|
+
bold: '\x1b[1m',
|
|
12
|
+
dim: '\x1b[2m',
|
|
13
|
+
green: '\x1b[32m',
|
|
14
|
+
cyan: '\x1b[36m',
|
|
15
|
+
yellow: '\x1b[33m',
|
|
16
|
+
red: '\x1b[31m',
|
|
17
|
+
magenta: '\x1b[35m',
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const TEMPLATES_DIR = path.join(__dirname, '..', 'templates');
|
|
21
|
+
const IS_WIN = process.platform === 'win32';
|
|
22
|
+
|
|
23
|
+
// ─── 사전 준비 체크 ───
|
|
24
|
+
function checkCommand(cmd) {
|
|
25
|
+
try {
|
|
26
|
+
const out = execSync(cmd, { stdio: 'pipe', timeout: 10000 }).toString().trim();
|
|
27
|
+
// 버전 문자열에서 숫자 부분만 추출
|
|
28
|
+
const ver = out.match(/(\d+\.\d+[\.\d]*)/);
|
|
29
|
+
return ver ? ver[1] : 'OK';
|
|
30
|
+
} catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function detectPython() {
|
|
36
|
+
const v3 = checkCommand('python3 --version');
|
|
37
|
+
if (v3) return { cmd: 'python3', version: v3 };
|
|
38
|
+
const v = checkCommand('python --version');
|
|
39
|
+
if (v && v.startsWith('3')) return { cmd: 'python', version: v };
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function getInstallHint(tool) {
|
|
44
|
+
const isMac = process.platform === 'darwin';
|
|
45
|
+
const hints = {
|
|
46
|
+
python: isMac
|
|
47
|
+
? 'brew install python3'
|
|
48
|
+
: IS_WIN
|
|
49
|
+
? 'https://www.python.org/downloads/ 에서 설치 (Add to PATH 체크)'
|
|
50
|
+
: 'sudo apt install python3',
|
|
51
|
+
git: isMac
|
|
52
|
+
? 'brew install git'
|
|
53
|
+
: IS_WIN
|
|
54
|
+
? 'https://git-scm.com/downloads 에서 설치'
|
|
55
|
+
: 'sudo apt install git',
|
|
56
|
+
tmux: isMac
|
|
57
|
+
? 'brew install tmux'
|
|
58
|
+
: 'sudo apt install tmux',
|
|
59
|
+
};
|
|
60
|
+
return hints[tool] || '';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function checkPrerequisites(rl) {
|
|
64
|
+
console.log(`${C.bold}사전 준비 확인 중...${C.reset}\n`);
|
|
65
|
+
|
|
66
|
+
const results = {};
|
|
67
|
+
let hasMissing = false;
|
|
68
|
+
|
|
69
|
+
// git
|
|
70
|
+
const gitVer = checkCommand('git --version');
|
|
71
|
+
if (gitVer) {
|
|
72
|
+
console.log(` ${C.green}✓${C.reset} git (${gitVer})`);
|
|
73
|
+
results.git = true;
|
|
74
|
+
} else {
|
|
75
|
+
console.log(` ${C.red}✗${C.reset} git — 설치 필요`);
|
|
76
|
+
results.git = false;
|
|
77
|
+
hasMissing = true;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// python
|
|
81
|
+
const py = detectPython();
|
|
82
|
+
if (py) {
|
|
83
|
+
console.log(` ${C.green}✓${C.reset} ${py.cmd} (${py.version})`);
|
|
84
|
+
results.python = py;
|
|
85
|
+
} else {
|
|
86
|
+
console.log(` ${C.red}✗${C.reset} python3 — 설치 필요`);
|
|
87
|
+
results.python = null;
|
|
88
|
+
hasMissing = true;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// claude
|
|
92
|
+
const claudeVer = checkCommand('claude --version');
|
|
93
|
+
if (claudeVer) {
|
|
94
|
+
console.log(` ${C.green}✓${C.reset} claude (${claudeVer})`);
|
|
95
|
+
results.claude = true;
|
|
96
|
+
} else {
|
|
97
|
+
console.log(` ${C.red}✗${C.reset} claude — 설치 필요`);
|
|
98
|
+
results.claude = false;
|
|
99
|
+
hasMissing = true;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// tmux (선택, Windows 제외)
|
|
103
|
+
if (!IS_WIN) {
|
|
104
|
+
const tmuxVer = checkCommand('tmux -V');
|
|
105
|
+
if (tmuxVer) {
|
|
106
|
+
console.log(` ${C.green}✓${C.reset} tmux (${tmuxVer})`);
|
|
107
|
+
} else {
|
|
108
|
+
console.log(` ${C.dim}-${C.reset} tmux — 미설치 (선택사항)`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
console.log('');
|
|
113
|
+
|
|
114
|
+
if (!hasMissing) return results;
|
|
115
|
+
|
|
116
|
+
// ─── 자동 설치 제안 ───
|
|
117
|
+
|
|
118
|
+
// Claude CLI 자동 설치
|
|
119
|
+
if (!results.claude && rl) {
|
|
120
|
+
const answer = await ask(rl, 'claude CLI를 설치할까요? (npm install -g @anthropic-ai/claude-code) [Y/n]', 'Y');
|
|
121
|
+
if (answer.toLowerCase() !== 'n') {
|
|
122
|
+
console.log(`\n ${C.dim}설치 중...${C.reset}`);
|
|
123
|
+
try {
|
|
124
|
+
execSync('npm install -g @anthropic-ai/claude-code', { stdio: 'inherit', timeout: 120000 });
|
|
125
|
+
console.log(` ${C.green}✓${C.reset} claude CLI 설치 완료\n`);
|
|
126
|
+
results.claude = true;
|
|
127
|
+
|
|
128
|
+
// 설치 후 권한 동의 안내
|
|
129
|
+
console.log(` ${C.yellow}!${C.reset} 최초 1회 권한 동의가 필요합니다:`);
|
|
130
|
+
console.log(` ${C.dim}claude --dangerously-skip-permissions${C.reset}`);
|
|
131
|
+
console.log(` ${C.dim}(동의 프롬프트 수락 후 Ctrl+C)${C.reset}\n`);
|
|
132
|
+
} catch {
|
|
133
|
+
console.log(` ${C.red}✗${C.reset} claude CLI 설치 실패\n`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// 나머지 누락 도구 안내
|
|
139
|
+
const missing = [];
|
|
140
|
+
if (!results.git) missing.push({ name: 'git', hint: getInstallHint('git') });
|
|
141
|
+
if (!results.python) missing.push({ name: 'python3', hint: getInstallHint('python') });
|
|
142
|
+
if (!results.claude) missing.push({ name: 'claude', hint: 'npm install -g @anthropic-ai/claude-code' });
|
|
143
|
+
|
|
144
|
+
if (missing.length > 0) {
|
|
145
|
+
console.log(`${C.red}${C.bold}아래 도구를 설치한 뒤 다시 실행해주세요:${C.reset}\n`);
|
|
146
|
+
for (const m of missing) {
|
|
147
|
+
console.log(` ${C.bold}${m.name}${C.reset}: ${C.cyan}${m.hint}${C.reset}`);
|
|
148
|
+
}
|
|
149
|
+
console.log('');
|
|
150
|
+
process.exit(1);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return results;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ─── 프로젝트 타입 정의 ───
|
|
157
|
+
const PROJECT_TYPES = {
|
|
158
|
+
'spring-boot': {
|
|
159
|
+
label: 'Spring Boot (Kotlin/Java)',
|
|
160
|
+
buildCmd: './gradlew build -x test --no-daemon',
|
|
161
|
+
testCmd: './gradlew test --no-daemon',
|
|
162
|
+
lintCmd: '',
|
|
163
|
+
},
|
|
164
|
+
'react-native': {
|
|
165
|
+
label: 'React Native (TypeScript)',
|
|
166
|
+
buildCmd: '',
|
|
167
|
+
testCmd: '',
|
|
168
|
+
lintCmd: 'npx tsc --noEmit',
|
|
169
|
+
},
|
|
170
|
+
nextjs: {
|
|
171
|
+
label: 'Next.js (TypeScript)',
|
|
172
|
+
buildCmd: 'npm run build',
|
|
173
|
+
testCmd: 'npm test',
|
|
174
|
+
lintCmd: 'npx next lint',
|
|
175
|
+
},
|
|
176
|
+
custom: {
|
|
177
|
+
label: 'Custom (직접 설정)',
|
|
178
|
+
buildCmd: '',
|
|
179
|
+
testCmd: '',
|
|
180
|
+
lintCmd: '',
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
// ─── CLI 인자 파싱 ───
|
|
185
|
+
function parseArgs() {
|
|
186
|
+
const args = process.argv.slice(2);
|
|
187
|
+
const parsed = {};
|
|
188
|
+
for (let i = 0; i < args.length; i++) {
|
|
189
|
+
if (args[i] === '--type' && args[i + 1]) parsed.type = args[++i];
|
|
190
|
+
else if (args[i] === '--name' && args[i + 1]) parsed.name = args[++i];
|
|
191
|
+
else if (args[i] === '--role' && args[i + 1]) parsed.role = args[++i];
|
|
192
|
+
else if (args[i] === '--figma-key' && args[i + 1]) parsed.figmaKey = args[++i];
|
|
193
|
+
else if (args[i] === '--interval' && args[i + 1]) parsed.interval = args[++i];
|
|
194
|
+
else if (args[i] === '--force' || args[i] === '-f') parsed.force = true;
|
|
195
|
+
else if (args[i] === '--help' || args[i] === '-h') {
|
|
196
|
+
console.log(`
|
|
197
|
+
사용법: sleepcode [옵션]
|
|
198
|
+
|
|
199
|
+
옵션 없이 실행하면 인터랙티브 모드로 동작합니다.
|
|
200
|
+
|
|
201
|
+
옵션:
|
|
202
|
+
--type <type> 프로젝트 타입 (spring-boot, react-native, nextjs, custom)
|
|
203
|
+
--name <name> 프로젝트 이름
|
|
204
|
+
--role <desc> AI 역할 설명
|
|
205
|
+
--figma-key <key> Figma API Key
|
|
206
|
+
--interval <sec> 반복 간격 (초, 기본 30)
|
|
207
|
+
-f, --force 기존 .sleepcode/ 덮어쓰기
|
|
208
|
+
-h, --help 도움말
|
|
209
|
+
`);
|
|
210
|
+
process.exit(0);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return parsed;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ─── 유틸 ───
|
|
217
|
+
function ask(rl, question, defaultVal) {
|
|
218
|
+
const suffix = defaultVal ? ` ${C.dim}(${defaultVal})${C.reset}` : '';
|
|
219
|
+
return new Promise((resolve) => {
|
|
220
|
+
rl.question(`${C.cyan}?${C.reset} ${question}${suffix}: `, (answer) => {
|
|
221
|
+
resolve(answer.trim() || defaultVal || '');
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function select(rl, question, options) {
|
|
227
|
+
return new Promise((resolve) => {
|
|
228
|
+
console.log(`\n${C.cyan}?${C.reset} ${question}`);
|
|
229
|
+
options.forEach((opt, i) => {
|
|
230
|
+
console.log(` ${C.bold}${i + 1})${C.reset} ${opt.label}`);
|
|
231
|
+
});
|
|
232
|
+
rl.question(`${C.cyan}>${C.reset} 번호 선택: `, (answer) => {
|
|
233
|
+
const idx = parseInt(answer, 10) - 1;
|
|
234
|
+
if (idx >= 0 && idx < options.length) {
|
|
235
|
+
resolve(options[idx]);
|
|
236
|
+
} else {
|
|
237
|
+
resolve(options[0]);
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function writeFile(filePath, content) {
|
|
244
|
+
const dir = path.dirname(filePath);
|
|
245
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
246
|
+
fs.writeFileSync(filePath, content);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function generateFiles(targetDir, { typeKey, projectName, role, buildCmd, testCmd, lintCmd, figmaKey, sleepInterval }) {
|
|
250
|
+
const scDir = path.join(targetDir, '.sleepcode');
|
|
251
|
+
const claudeDir = path.join(targetDir, '.claude');
|
|
252
|
+
fs.mkdirSync(path.join(scDir, 'docs'), { recursive: true });
|
|
253
|
+
fs.mkdirSync(path.join(scDir, 'scripts'), { recursive: true });
|
|
254
|
+
fs.mkdirSync(path.join(scDir, 'logs'), { recursive: true });
|
|
255
|
+
fs.mkdirSync(claudeDir, { recursive: true });
|
|
256
|
+
|
|
257
|
+
// 스크립트 파일 → scripts/ 하위로 복사 (OS별 분기)
|
|
258
|
+
const scriptFiles = IS_WIN
|
|
259
|
+
? ['ai_worker.ps1', 'run_forever.ps1']
|
|
260
|
+
: ['ai_worker.sh', 'run_forever.sh'];
|
|
261
|
+
const allScriptFiles = [...scriptFiles, 'log_filter.py'];
|
|
262
|
+
|
|
263
|
+
for (const file of allScriptFiles) {
|
|
264
|
+
const src = path.join(TEMPLATES_DIR, 'common', file);
|
|
265
|
+
const dest = path.join(scDir, 'scripts', file);
|
|
266
|
+
if (fs.existsSync(src)) {
|
|
267
|
+
let content = fs.readFileSync(src, 'utf-8');
|
|
268
|
+
content = content.replace(/\{\{SLEEP_INTERVAL\}\}/g, sleepInterval);
|
|
269
|
+
fs.writeFileSync(dest, content);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// README.md → .sleepcode/ 루트에 복사
|
|
274
|
+
const readmeSrc = path.join(TEMPLATES_DIR, 'common', 'README.md');
|
|
275
|
+
if (fs.existsSync(readmeSrc)) {
|
|
276
|
+
fs.writeFileSync(path.join(scDir, 'README.md'), fs.readFileSync(readmeSrc, 'utf-8'));
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// 실행 권한 (Unix만)
|
|
280
|
+
if (!IS_WIN) {
|
|
281
|
+
fs.chmodSync(path.join(scDir, 'scripts', 'ai_worker.sh'), 0o755);
|
|
282
|
+
fs.chmodSync(path.join(scDir, 'scripts', 'run_forever.sh'), 0o755);
|
|
283
|
+
fs.chmodSync(path.join(scDir, 'scripts', 'log_filter.py'), 0o755);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// docs/.gitkeep
|
|
287
|
+
writeFile(path.join(scDir, 'docs', '.gitkeep'), '');
|
|
288
|
+
|
|
289
|
+
// tasks.md
|
|
290
|
+
writeFile(
|
|
291
|
+
path.join(scDir, 'tasks.md'),
|
|
292
|
+
`# 작업 목록
|
|
293
|
+
|
|
294
|
+
아래 태스크를 순서대로 진행하세요. 완료한 항목은 \`[x]\`로 체크하세요.
|
|
295
|
+
|
|
296
|
+
---
|
|
297
|
+
|
|
298
|
+
- [ ] 여기에 첫 번째 작업을 적어주세요
|
|
299
|
+
`
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
// rules.md
|
|
303
|
+
const rulesTemplate = path.join(TEMPLATES_DIR, 'rules', `${typeKey}.md`);
|
|
304
|
+
if (fs.existsSync(rulesTemplate)) {
|
|
305
|
+
let rules = fs.readFileSync(rulesTemplate, 'utf-8');
|
|
306
|
+
rules = rules.replace(/\{\{PROJECT_NAME\}\}/g, projectName);
|
|
307
|
+
rules = rules.replace(/\{\{ROLE\}\}/g, role);
|
|
308
|
+
rules = rules.replace(/\{\{BUILD_CMD\}\}/g, buildCmd);
|
|
309
|
+
rules = rules.replace(/\{\{TEST_CMD\}\}/g, testCmd);
|
|
310
|
+
rules = rules.replace(/\{\{LINT_CMD\}\}/g, lintCmd);
|
|
311
|
+
rules = rules.replace(/\{\{FIGMA_API_KEY\}\}/g, figmaKey);
|
|
312
|
+
|
|
313
|
+
if (!figmaKey) {
|
|
314
|
+
rules = rules.replace(/\n## Figma[\s\S]*?(?=\n## |$)/, '');
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
writeFile(path.join(scDir, 'rules.md'), rules);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// settings.local.json
|
|
321
|
+
const settingsTemplate = path.join(TEMPLATES_DIR, 'settings', `${typeKey}.json`);
|
|
322
|
+
if (fs.existsSync(settingsTemplate)) {
|
|
323
|
+
const content = fs.readFileSync(settingsTemplate, 'utf-8');
|
|
324
|
+
fs.writeFileSync(path.join(claudeDir, 'settings.local.json'), content);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// .gitignore
|
|
328
|
+
const gitignorePath = path.join(targetDir, '.gitignore');
|
|
329
|
+
if (fs.existsSync(gitignorePath)) {
|
|
330
|
+
const gitignore = fs.readFileSync(gitignorePath, 'utf-8');
|
|
331
|
+
if (!gitignore.includes('.sleepcode/logs/')) {
|
|
332
|
+
fs.appendFileSync(gitignorePath, '\n# AI worker logs\n.sleepcode/logs/\n');
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function printResult() {
|
|
338
|
+
const workerScript = IS_WIN ? 'ai_worker.ps1' : 'ai_worker.sh';
|
|
339
|
+
const foreverScript = IS_WIN ? 'run_forever.ps1' : 'run_forever.sh';
|
|
340
|
+
|
|
341
|
+
console.log(`\n${C.bold}파일 생성 완료:${C.reset}\n`);
|
|
342
|
+
console.log(` ${C.green}✓${C.reset} .sleepcode/rules.md ${C.dim}← 수정하세요${C.reset}`);
|
|
343
|
+
console.log(` ${C.green}✓${C.reset} .sleepcode/tasks.md ${C.dim}← 수정하세요${C.reset}`);
|
|
344
|
+
console.log(` ${C.green}✓${C.reset} .sleepcode/docs/ ${C.dim}← 참고자료 추가${C.reset}`);
|
|
345
|
+
console.log(` ${C.green}✓${C.reset} .sleepcode/scripts/${workerScript}`);
|
|
346
|
+
console.log(` ${C.green}✓${C.reset} .sleepcode/scripts/${foreverScript}`);
|
|
347
|
+
console.log(` ${C.green}✓${C.reset} .sleepcode/scripts/log_filter.py`);
|
|
348
|
+
console.log(` ${C.green}✓${C.reset} .sleepcode/README.md`);
|
|
349
|
+
console.log(` ${C.green}✓${C.reset} .claude/settings.local.json`);
|
|
350
|
+
|
|
351
|
+
if (IS_WIN) {
|
|
352
|
+
console.log(`
|
|
353
|
+
${C.bold}${C.green}완료!${C.reset} 다음 단계:
|
|
354
|
+
|
|
355
|
+
${C.bold}1.${C.reset} .sleepcode/rules.md 를 프로젝트에 맞게 수정
|
|
356
|
+
${C.bold}2.${C.reset} .sleepcode/tasks.md 에 작업 목록 작성
|
|
357
|
+
${C.bold}3.${C.reset} 실행 (PowerShell):
|
|
358
|
+
${C.dim}# 1회 실행${C.reset}
|
|
359
|
+
powershell -File .\\.sleepcode\\scripts\\ai_worker.ps1
|
|
360
|
+
|
|
361
|
+
${C.dim}# 무한 루프${C.reset}
|
|
362
|
+
powershell -File .\\.sleepcode\\scripts\\run_forever.ps1
|
|
363
|
+
`);
|
|
364
|
+
} else {
|
|
365
|
+
console.log(`
|
|
366
|
+
${C.bold}${C.green}완료!${C.reset} 다음 단계:
|
|
367
|
+
|
|
368
|
+
${C.bold}1.${C.reset} .sleepcode/rules.md 를 프로젝트에 맞게 수정
|
|
369
|
+
${C.bold}2.${C.reset} .sleepcode/tasks.md 에 작업 목록 작성
|
|
370
|
+
${C.bold}3.${C.reset} 실행:
|
|
371
|
+
${C.dim}# 1회 실행${C.reset}
|
|
372
|
+
./.sleepcode/scripts/ai_worker.sh
|
|
373
|
+
|
|
374
|
+
${C.dim}# 무한 루프 (tmux)${C.reset}
|
|
375
|
+
tmux new -s ai './.sleepcode/scripts/run_forever.sh'
|
|
376
|
+
`);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// ─── 메인 ───
|
|
381
|
+
async function main() {
|
|
382
|
+
const targetDir = process.cwd();
|
|
383
|
+
const cliArgs = parseArgs();
|
|
384
|
+
|
|
385
|
+
console.log(`
|
|
386
|
+
${C.bold}${C.magenta} ╔══════════════════════════════════╗
|
|
387
|
+
║ sleepcode ║
|
|
388
|
+
║ AI codes while you sleep ║
|
|
389
|
+
╚══════════════════════════════════╝${C.reset}
|
|
390
|
+
`);
|
|
391
|
+
|
|
392
|
+
// 비대화형 모드: --type 이 있으면 인터랙티브 스킵
|
|
393
|
+
if (cliArgs.type) {
|
|
394
|
+
// 비대화형: 사전 준비 체크 (자동 설치 제안 없음)
|
|
395
|
+
await checkPrerequisites(null);
|
|
396
|
+
|
|
397
|
+
const typeKey = cliArgs.type;
|
|
398
|
+
if (!PROJECT_TYPES[typeKey]) {
|
|
399
|
+
console.error(`${C.red}알 수 없는 타입: ${typeKey}${C.reset}`);
|
|
400
|
+
console.error(`사용 가능: ${Object.keys(PROJECT_TYPES).join(', ')}`);
|
|
401
|
+
process.exit(1);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (fs.existsSync(path.join(targetDir, '.sleepcode')) && !cliArgs.force) {
|
|
405
|
+
console.error(`${C.red}.sleepcode/ 폴더가 이미 존재합니다. --force 로 덮어쓰세요.${C.reset}`);
|
|
406
|
+
process.exit(1);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const typeConfig = PROJECT_TYPES[typeKey];
|
|
410
|
+
const projectName = cliArgs.name || path.basename(targetDir);
|
|
411
|
+
const role = cliArgs.role || `${projectName} 서비스 개발`;
|
|
412
|
+
const figmaKey = cliArgs.figmaKey || '';
|
|
413
|
+
const sleepInterval = cliArgs.interval || '30';
|
|
414
|
+
|
|
415
|
+
console.log(`${C.dim}타입: ${typeConfig.label}${C.reset}`);
|
|
416
|
+
console.log(`${C.dim}이름: ${projectName}${C.reset}`);
|
|
417
|
+
console.log(`${C.dim}역할: ${role}${C.reset}`);
|
|
418
|
+
|
|
419
|
+
generateFiles(targetDir, {
|
|
420
|
+
typeKey,
|
|
421
|
+
projectName,
|
|
422
|
+
role,
|
|
423
|
+
buildCmd: typeConfig.buildCmd,
|
|
424
|
+
testCmd: typeConfig.testCmd,
|
|
425
|
+
lintCmd: typeConfig.lintCmd,
|
|
426
|
+
figmaKey,
|
|
427
|
+
sleepInterval,
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
printResult();
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// 인터랙티브 모드
|
|
435
|
+
const rl = readline.createInterface({
|
|
436
|
+
input: process.stdin,
|
|
437
|
+
output: process.stdout,
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
try {
|
|
441
|
+
// 인터랙티브: 사전 준비 체크 (자동 설치 제안 포함)
|
|
442
|
+
await checkPrerequisites(rl);
|
|
443
|
+
|
|
444
|
+
if (fs.existsSync(path.join(targetDir, '.sleepcode'))) {
|
|
445
|
+
console.log(`${C.yellow}⚠ .sleepcode/ 폴더가 이미 존재합니다.${C.reset}`);
|
|
446
|
+
const overwrite = await ask(rl, '덮어쓸까요? (y/N)', 'N');
|
|
447
|
+
if (overwrite.toLowerCase() !== 'y') {
|
|
448
|
+
console.log('취소됨.');
|
|
449
|
+
rl.close();
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const typeOptions = Object.entries(PROJECT_TYPES).map(([key, val]) => ({
|
|
455
|
+
key,
|
|
456
|
+
label: val.label,
|
|
457
|
+
}));
|
|
458
|
+
const selectedType = await select(rl, '프로젝트 타입', typeOptions);
|
|
459
|
+
const typeKey = selectedType.key;
|
|
460
|
+
const typeConfig = PROJECT_TYPES[typeKey];
|
|
461
|
+
|
|
462
|
+
const projectName = await ask(rl, '프로젝트 이름', path.basename(targetDir));
|
|
463
|
+
const role = await ask(rl, 'AI 역할 설명', `${projectName} 서비스 개발`);
|
|
464
|
+
|
|
465
|
+
let buildCmd = typeConfig.buildCmd;
|
|
466
|
+
let testCmd = typeConfig.testCmd;
|
|
467
|
+
let lintCmd = typeConfig.lintCmd;
|
|
468
|
+
|
|
469
|
+
if (typeKey === 'custom') {
|
|
470
|
+
buildCmd = await ask(rl, '빌드 커맨드 (없으면 Enter)', '');
|
|
471
|
+
testCmd = await ask(rl, '테스트 커맨드 (없으면 Enter)', '');
|
|
472
|
+
lintCmd = await ask(rl, '린트 커맨드 (없으면 Enter)', '');
|
|
473
|
+
} else {
|
|
474
|
+
console.log(`${C.dim} 빌드: ${buildCmd || '(없음)'}${C.reset}`);
|
|
475
|
+
console.log(`${C.dim} 테스트: ${testCmd || '(없음)'}${C.reset}`);
|
|
476
|
+
console.log(`${C.dim} 린트: ${lintCmd || '(없음)'}${C.reset}`);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const figmaKey = await ask(rl, 'Figma API Key (없으면 Enter)', '');
|
|
480
|
+
const sleepInterval = await ask(rl, '반복 간격 (초)', '30');
|
|
481
|
+
|
|
482
|
+
rl.close();
|
|
483
|
+
|
|
484
|
+
generateFiles(targetDir, {
|
|
485
|
+
typeKey,
|
|
486
|
+
projectName,
|
|
487
|
+
role,
|
|
488
|
+
buildCmd,
|
|
489
|
+
testCmd,
|
|
490
|
+
lintCmd,
|
|
491
|
+
figmaKey,
|
|
492
|
+
sleepInterval,
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
printResult();
|
|
496
|
+
} catch (e) {
|
|
497
|
+
console.error(`${C.red}오류: ${e.message}${C.reset}`);
|
|
498
|
+
rl.close();
|
|
499
|
+
process.exit(1);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
main();
|