deel-local-cli 1.0.2 → 1.1.1
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/README.en.md +381 -7
- package/README.md +493 -9
- package/bin/deel.js +266 -234
- package/package.json +4 -3
- package/src/agent/budget.js +167 -0
- package/src/agent/grade.js +207 -0
- package/src/agent/loop.js +778 -574
- package/src/agent/modes.js +109 -28
- package/src/agent/project.js +171 -0
- package/src/agent/route.js +55 -3
- package/src/agent/session.js +131 -12
- package/src/backend/http.js +41 -4
- package/src/backend/learn.js +46 -4
- package/src/commands.js +178 -3
- package/src/oneshot.js +390 -327
- package/src/preview/serve.js +326 -0
- package/src/repl.js +1184 -887
- package/src/safety/audit.js +3 -1
- package/src/skills/builtin//352/262/200/354/202/254-/353/250/274/354/240/200/SKILL.md +64 -0
- package/src/skills/builtin//352/271/212/354/235/264/354/236/210/352/262/214-/353/247/214/353/223/244/352/270/260/SKILL.md +78 -0
- package/src/skills/builtin//353/201/235/352/271/214/354/247/200-/355/225/230/352/270/260/SKILL.md +65 -0
- package/src/skills/builtin//354/212/244/354/212/244/353/241/234-/352/262/200/355/206/240/SKILL.md +74 -0
- package/src/skills/builtin//354/260/224/353/237/254/353/263/264/352/270/260/SKILL.md +59 -0
- package/src/skills/builtin//354/260/250/352/267/274/354/260/250/352/267/274-/353/224/224/353/262/204/352/271/205/SKILL.md +73 -0
- package/src/skills/builtin//354/275/224/353/223/234-/354/244/204/354/235/264/352/270/260/SKILL.md +66 -0
- package/src/skills/discover.js +17 -2
- package/src/tools/index.js +500 -107
- package/src/tools/jobs.js +670 -0
- package/src/tools/outline.js +331 -0
- package/src/tools/task.js +153 -0
- package/src/tools/verify.js +306 -0
- package/src/tools/webfetch.js +152 -16
- package/src/ui/inputbox.js +102 -7
- package/src/ui/motion.js +212 -0
- package/src/ui/screen.js +51 -3
- package/src/ui/status.js +69 -4
- package/src/ui/working.js +8 -1
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 만든 웹을 그 자리에서 띄워 본다.
|
|
3
|
+
*
|
|
4
|
+
* ── 왜 만드나 ───────────────────────────────────────────────────────────
|
|
5
|
+
*
|
|
6
|
+
* 웹을 하나 만들어 놓고 눈으로 보려면 여태 이랬다 —
|
|
7
|
+
* · 파일을 두 번 눌러 연다 → file:// 라 fetch·모듈·WebGL 텍스처가 막힌다
|
|
8
|
+
* · GitHub Pages 에 올린다 → 한 글자 고칠 때마다 커밋·푸시·기다림
|
|
9
|
+
* · 아무 정적 서버나 깐다 → 미승인 SW 반입 금지에 걸린다
|
|
10
|
+
*
|
|
11
|
+
* Node 에 http 가 들어 있으므로 셋 다 필요 없다. 의존성은 그대로 0개다.
|
|
12
|
+
*
|
|
13
|
+
* ── 무엇을 조심했나 ─────────────────────────────────────────────────────
|
|
14
|
+
*
|
|
15
|
+
* 서버를 띄운다는 것은 **내 디스크를 남에게 열어 주는 일**이다. 그래서
|
|
16
|
+
*
|
|
17
|
+
* 1. 127.0.0.1 에만 묶는다. 0.0.0.0 은 아예 못 쓰게 해 뒀다 —
|
|
18
|
+
* 같은 사무실 망에서 아무나 내 소스를 읽게 된다.
|
|
19
|
+
* 2. 포트는 0(커널이 빈 것을 준다). 고정 포트는 남이 쓰던 것을 뺏는다.
|
|
20
|
+
* 3. 경로는 guard 의 scope 로만 푼다. `../`·심볼릭 링크·정션을 이미
|
|
21
|
+
* 거기서 막고 있으므로 여기서 또 짜지 않는다. 두 벌이 되면 한쪽만 고쳐진다.
|
|
22
|
+
* 4. 파일을 **주기만** 한다. PUT·POST·DELETE 는 받지 않는다.
|
|
23
|
+
*/
|
|
24
|
+
import { createServer } from 'node:http';
|
|
25
|
+
import { createReadStream, statSync, existsSync, readdirSync, watch } from 'node:fs';
|
|
26
|
+
import { join, extname, relative, sep } from 'node:path';
|
|
27
|
+
import { spawn } from 'node:child_process';
|
|
28
|
+
|
|
29
|
+
// 확장자 → 형식. 없는 것은 그냥 내려받게 둔다.
|
|
30
|
+
// glb·gltf·wasm 을 넣어 뒀다 — 형식이 틀리면 Three.js 가 조용히 아무것도 안 그린다.
|
|
31
|
+
const 형식 = {
|
|
32
|
+
'.html': 'text/html; charset=utf-8', '.htm': 'text/html; charset=utf-8',
|
|
33
|
+
'.css': 'text/css; charset=utf-8',
|
|
34
|
+
'.js': 'text/javascript; charset=utf-8', '.mjs': 'text/javascript; charset=utf-8',
|
|
35
|
+
'.json': 'application/json; charset=utf-8', '.map': 'application/json; charset=utf-8',
|
|
36
|
+
'.txt': 'text/plain; charset=utf-8', '.md': 'text/plain; charset=utf-8',
|
|
37
|
+
'.csv': 'text/csv; charset=utf-8',
|
|
38
|
+
'.svg': 'image/svg+xml', '.png': 'image/png', '.ico': 'image/x-icon',
|
|
39
|
+
'.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif', '.webp': 'image/webp',
|
|
40
|
+
'.woff': 'font/woff', '.woff2': 'font/woff2', '.ttf': 'font/ttf', '.otf': 'font/otf',
|
|
41
|
+
'.mp4': 'video/mp4', '.webm': 'video/webm', '.mp3': 'audio/mpeg', '.wav': 'audio/wav',
|
|
42
|
+
'.wasm': 'application/wasm',
|
|
43
|
+
'.glb': 'model/gltf-binary', '.gltf': 'model/gltf+json',
|
|
44
|
+
'.pdf': 'application/pdf',
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// 고친 것이 바로 보이게 하는 조각. HTML 에만 끼워 넣는다.
|
|
48
|
+
//
|
|
49
|
+
// 파일을 고치면 화면이 저절로 새로 뜬다. 이게 없으면 결국 브라우저를 손으로
|
|
50
|
+
// 새로 고치게 되고, 그러면 굳이 여기서 띄울 이유가 절반은 사라진다.
|
|
51
|
+
const 되살림 = `
|
|
52
|
+
<script>/* deel 미리보기 — 파일이 바뀌면 새로 뜬다 */
|
|
53
|
+
(function(){try{
|
|
54
|
+
var s=new EventSource('/__deel__/살아있나');
|
|
55
|
+
s.onmessage=function(e){ if(e.data==='다시')location.reload(); };
|
|
56
|
+
// 서버가 꺼지면 EventSource 가 계속 다시 붙으려 한다. 조용히 놔둔다 —
|
|
57
|
+
// deel 을 다시 띄우면 그대로 이어 붙어서 화면을 새로 고치지 않아도 된다.
|
|
58
|
+
}catch(err){}})();
|
|
59
|
+
</script>`;
|
|
60
|
+
|
|
61
|
+
const 안전한경로 = (u) => {
|
|
62
|
+
// %00 이나 널바이트가 섞이면 아래 fs 가 경로를 잘라 읽는다. 통째로 거절한다.
|
|
63
|
+
let p;
|
|
64
|
+
try { p = decodeURIComponent(String(u).split('?')[0].split('#')[0]); } catch { return null; }
|
|
65
|
+
if (p.includes('\0')) return null;
|
|
66
|
+
return p.replace(/^\/+/, '');
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* 폴더 하나를 띄운다.
|
|
71
|
+
*
|
|
72
|
+
* @param {object} o
|
|
73
|
+
* @param {string} o.뿌리 띄울 폴더 (이미 scope 로 푼 절대 경로여야 한다)
|
|
74
|
+
* @param {object} o.scope guard 의 makeScope 결과. 경로는 전부 이걸로 푼다.
|
|
75
|
+
* @param {boolean} [o.되살리기=true] 파일이 바뀌면 새로 뜨게 할지
|
|
76
|
+
* @returns {Promise<{url:string, port:number, 닫기:Function, 바뀐수:Function}>}
|
|
77
|
+
*/
|
|
78
|
+
export function 띄우기({ 뿌리, scope, 되살리기 = true }) {
|
|
79
|
+
const 듣는이들 = new Set(); // 살아 있는 EventSource 응답들
|
|
80
|
+
let 바뀐수 = 0;
|
|
81
|
+
let 감시 = null;
|
|
82
|
+
let 늦추기 = null;
|
|
83
|
+
|
|
84
|
+
const 알리기 = () => {
|
|
85
|
+
바뀐수 += 1;
|
|
86
|
+
for (const res of 듣는이들) {
|
|
87
|
+
try { res.write('data: 다시\n\n'); } catch { 듣는이들.delete(res); }
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const srv = createServer((req, res) => {
|
|
92
|
+
// 주기만 한다. 받는 길은 아예 열지 않는다.
|
|
93
|
+
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
|
94
|
+
res.writeHead(405, { allow: 'GET, HEAD' });
|
|
95
|
+
return res.end('GET 만 받습니다');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const 길 = 안전한경로(req.url);
|
|
99
|
+
if (길 === null) { res.writeHead(400); return res.end('주소가 이상합니다'); }
|
|
100
|
+
|
|
101
|
+
// 되살림 통로. 열어 두고 파일이 바뀔 때마다 한 줄씩 흘린다.
|
|
102
|
+
if (길 === '__deel__/살아있나') {
|
|
103
|
+
res.writeHead(200, {
|
|
104
|
+
'content-type': 'text/event-stream; charset=utf-8',
|
|
105
|
+
'cache-control': 'no-cache',
|
|
106
|
+
connection: 'keep-alive',
|
|
107
|
+
});
|
|
108
|
+
res.write(': 붙었습니다\n\n');
|
|
109
|
+
듣는이들.add(res);
|
|
110
|
+
req.on('close', () => 듣는이들.delete(res));
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
let abs;
|
|
115
|
+
try {
|
|
116
|
+
abs = scope.resolve(길 ? join(뿌리, 길) : 뿌리);
|
|
117
|
+
// scope 는 작업 범위까지만 본다. 띄운 폴더 밖도 막아야 한다 —
|
|
118
|
+
// 아니면 /../다른폴더/비밀.env 로 프로젝트 전체가 열린다.
|
|
119
|
+
const 안쪽 = relative(뿌리, abs);
|
|
120
|
+
if (안쪽.startsWith('..') || (안쪽 !== '' && 안쪽.split(sep)[0] === '..')) {
|
|
121
|
+
res.writeHead(403); return res.end('띄운 폴더 밖입니다');
|
|
122
|
+
}
|
|
123
|
+
} catch {
|
|
124
|
+
res.writeHead(403);
|
|
125
|
+
return res.end('작업 범위 밖입니다');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (!existsSync(abs)) {
|
|
129
|
+
/*
|
|
130
|
+
* 주소로 화면을 나누는 앱(React Router·Vue Router 같은 것)을 위한 길.
|
|
131
|
+
*
|
|
132
|
+
* `/설정` 같은 주소는 디스크에 파일이 없다. 첫 화면에서 눌러 들어가면
|
|
133
|
+
* 되는데 **거기서 새로고침을 하면 404** 가 뜬다. 만든 사람은 "내 앱이
|
|
134
|
+
* 깨졌나" 싶지만 앱은 멀쩡하고 서버가 모르는 것뿐이다.
|
|
135
|
+
*
|
|
136
|
+
* 그래서 확장자가 없고 HTML 을 달라는 요청이면 첫 장을 준다.
|
|
137
|
+
* 확장자가 있는 것(app.js·a.css)에는 절대 안 한다 — 없는 스크립트에
|
|
138
|
+
* HTML 을 돌려주면 브라우저가 "Unexpected token '<'" 로 죽는데,
|
|
139
|
+
* 그게 진짜 원인(파일 이름 오타)을 완전히 가린다.
|
|
140
|
+
*/
|
|
141
|
+
const 첫장 = join(뿌리, 'index.html');
|
|
142
|
+
const 확장자없나 = !extname(길);
|
|
143
|
+
const html달라나 = String(req.headers.accept ?? '').includes('text/html');
|
|
144
|
+
if (확장자없나 && html달라나 && existsSync(첫장)) return 파일주기(첫장, req, res, 되살리기);
|
|
145
|
+
res.writeHead(404);
|
|
146
|
+
return res.end('없는 파일입니다');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
let st;
|
|
150
|
+
try { st = statSync(abs); } catch { res.writeHead(404); return res.end('못 읽습니다'); }
|
|
151
|
+
|
|
152
|
+
if (st.isDirectory()) {
|
|
153
|
+
/*
|
|
154
|
+
* 끝에 슬래시가 없으면 **반드시** 붙여서 다시 보낸다.
|
|
155
|
+
*
|
|
156
|
+
* 이걸 안 하면 안엣것이 통째로 깨진다. `/docs` 로 들어오면 index.html 은
|
|
157
|
+
* 나오는데, 그 안의 `./app.js` 를 브라우저가 `/app.js` 로 푼다 —
|
|
158
|
+
* 실제로는 `/docs/app.js` 인데 404 가 난다.
|
|
159
|
+
* 화면은 흰데 오류는 콘솔에만 있어서 "왜 안 되지" 로 한참 헤매게 된다.
|
|
160
|
+
*/
|
|
161
|
+
if (길 !== '' && !req.url.split('?')[0].endsWith('/')) {
|
|
162
|
+
const [, 물음표] = req.url.split(/(\?.*)$/);
|
|
163
|
+
// 헤더는 ASCII 만 실린다. 한글 폴더 이름을 그대로 넣으면 ERR_INVALID_CHAR 로
|
|
164
|
+
// **서버가 죽는다.** 미리보기 하나 켰다가 deel 이 통째로 끝나는 셈이다.
|
|
165
|
+
// 길은 이미 디코드된 값이므로 다시 인코드해서 싣는다.
|
|
166
|
+
res.writeHead(301, { location: `${encodeURI(`/${길}/`)}${물음표 ?? ''}` });
|
|
167
|
+
return res.end();
|
|
168
|
+
}
|
|
169
|
+
const 첫장 = join(abs, 'index.html');
|
|
170
|
+
if (existsSync(첫장)) return 파일주기(첫장, req, res, 되살리기);
|
|
171
|
+
return 목록주기(abs, 뿌리, res);
|
|
172
|
+
}
|
|
173
|
+
return 파일주기(abs, req, res, 되살리기);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
return new Promise((done, fail) => {
|
|
177
|
+
srv.on('error', fail);
|
|
178
|
+
// 127.0.0.1 만. 여기를 0.0.0.0 으로 바꾸면 같은 망의 아무나 내 소스를 읽는다.
|
|
179
|
+
srv.listen(0, '127.0.0.1', () => {
|
|
180
|
+
const port = srv.address().port;
|
|
181
|
+
|
|
182
|
+
if (되살리기) {
|
|
183
|
+
try {
|
|
184
|
+
감시 = watch(뿌리, { recursive: true }, () => {
|
|
185
|
+
// 저장 한 번에 이벤트가 여러 개 온다. 몰아서 한 번만 알린다.
|
|
186
|
+
clearTimeout(늦추기);
|
|
187
|
+
늦추기 = setTimeout(알리기, 120);
|
|
188
|
+
// 이 시계가 프로그램을 붙잡고 있으면 안 된다.
|
|
189
|
+
늦추기.unref?.();
|
|
190
|
+
});
|
|
191
|
+
} catch {
|
|
192
|
+
// 리눅스 옛 커널 등에서 recursive 가 안 될 수 있다. 그때는 되살림만 없다.
|
|
193
|
+
감시 = null;
|
|
194
|
+
}
|
|
195
|
+
감시?.unref?.();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
done({
|
|
199
|
+
url: `http://127.0.0.1:${port}/`,
|
|
200
|
+
port,
|
|
201
|
+
되살아나나: !!감시,
|
|
202
|
+
바뀐수: () => 바뀐수,
|
|
203
|
+
듣는수: () => 듣는이들.size,
|
|
204
|
+
닫기() {
|
|
205
|
+
clearTimeout(늦추기);
|
|
206
|
+
try { 감시?.close(); } catch { /* 이미 닫혔다 */ }
|
|
207
|
+
// 열어 둔 통로를 안 끊으면 서버가 안 닫힌다 — 프로그램이 안 끝난다.
|
|
208
|
+
for (const r of 듣는이들) { try { r.end(); } catch { /* 그만 */ } }
|
|
209
|
+
듣는이들.clear();
|
|
210
|
+
return new Promise((r) => srv.close(r));
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function 파일주기(abs, req, res, 되살리기) {
|
|
218
|
+
const ext = extname(abs).toLowerCase();
|
|
219
|
+
const mime = 형식[ext] ?? 'application/octet-stream';
|
|
220
|
+
let st;
|
|
221
|
+
try { st = statSync(abs); } catch { res.writeHead(404); return res.end(); }
|
|
222
|
+
|
|
223
|
+
// HTML 에는 되살림 조각을 끼워 넣는다. 길이가 달라지므로 흘려보내지 않고 통째로 읽는다.
|
|
224
|
+
if (되살리기 && (ext === '.html' || ext === '.htm')) {
|
|
225
|
+
let 글 = '';
|
|
226
|
+
const s = createReadStream(abs, 'utf8');
|
|
227
|
+
s.on('data', (d) => { 글 += d; });
|
|
228
|
+
s.on('error', () => { res.writeHead(500); res.end(); });
|
|
229
|
+
s.on('end', () => {
|
|
230
|
+
// </body> 앞에 넣는 게 정석이지만 없는 문서도 많다. 없으면 그냥 뒤에 붙인다.
|
|
231
|
+
const 몸 = 글.includes('</body>') ? 글.replace('</body>', `${되살림}\n</body>`) : 글 + 되살림;
|
|
232
|
+
const buf = Buffer.from(몸, 'utf8');
|
|
233
|
+
res.writeHead(200, { 'content-type': mime, 'content-length': buf.length, 'cache-control': 'no-store' });
|
|
234
|
+
res.end(req.method === 'HEAD' ? undefined : buf);
|
|
235
|
+
});
|
|
236
|
+
return undefined;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/*
|
|
240
|
+
* 동영상·소리는 브라우저가 조각으로 달라고 한다(Range).
|
|
241
|
+
* 이걸 안 받아 주면 <video> 가 아예 안 돈다 — 파일은 멀쩡한데 화면만 검다.
|
|
242
|
+
*/
|
|
243
|
+
const range = req.headers.range;
|
|
244
|
+
const m = range && /^bytes=(\d*)-(\d*)$/.exec(String(range).trim());
|
|
245
|
+
if (m && st.size > 0) {
|
|
246
|
+
let 처음 = m[1] === '' ? null : Number(m[1]);
|
|
247
|
+
let 끝 = m[2] === '' ? null : Number(m[2]);
|
|
248
|
+
if (처음 === null && 끝 !== null) { 처음 = Math.max(0, st.size - 끝); 끝 = st.size - 1; }
|
|
249
|
+
else { 처음 = 처음 ?? 0; 끝 = 끝 === null ? st.size - 1 : Math.min(끝, st.size - 1); }
|
|
250
|
+
if (!Number.isFinite(처음) || !Number.isFinite(끝) || 처음 > 끝 || 처음 >= st.size) {
|
|
251
|
+
res.writeHead(416, { 'content-range': `bytes */${st.size}` });
|
|
252
|
+
return res.end();
|
|
253
|
+
}
|
|
254
|
+
res.writeHead(206, {
|
|
255
|
+
'content-type': mime,
|
|
256
|
+
'content-range': `bytes ${처음}-${끝}/${st.size}`,
|
|
257
|
+
'accept-ranges': 'bytes',
|
|
258
|
+
'content-length': 끝 - 처음 + 1,
|
|
259
|
+
});
|
|
260
|
+
if (req.method === 'HEAD') return res.end();
|
|
261
|
+
return createReadStream(abs, { start: 처음, end: 끝 }).pipe(res);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
res.writeHead(200, {
|
|
265
|
+
'content-type': mime,
|
|
266
|
+
'content-length': st.size,
|
|
267
|
+
'accept-ranges': 'bytes',
|
|
268
|
+
// 고친 것이 바로 보여야 한다. 미리보기에서 캐시는 도움이 안 된다.
|
|
269
|
+
'cache-control': 'no-store',
|
|
270
|
+
});
|
|
271
|
+
if (req.method === 'HEAD') return res.end();
|
|
272
|
+
return createReadStream(abs).pipe(res);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// index.html 이 없는 폴더. 무엇이 있는지라도 보여 준다 — 흰 화면보다 낫다.
|
|
276
|
+
function 목록주기(abs, 뿌리, res) {
|
|
277
|
+
let 것들 = [];
|
|
278
|
+
try { 것들 = readdirSync(abs, { withFileTypes: true }); } catch { /* 못 읽으면 빈 채로 */ }
|
|
279
|
+
const 여기 = relative(뿌리, abs).split(sep).join('/');
|
|
280
|
+
const 줄 = (e) => {
|
|
281
|
+
const 이름 = e.name + (e.isDirectory() ? '/' : '');
|
|
282
|
+
return `<li><a href="${encodeURIComponent(e.name)}${e.isDirectory() ? '/' : ''}">${이름}</a></li>`;
|
|
283
|
+
};
|
|
284
|
+
const 몸 = `<!doctype html><meta charset="utf-8"><title>${여기 || '/'}</title>`
|
|
285
|
+
+ '<style>body{font:14px/1.7 ui-monospace,monospace;max-width:44rem;margin:3rem auto;padding:0 1rem}'
|
|
286
|
+
+ 'h1{font-size:1rem;color:#888;font-weight:400}a{color:#0a7}li{list-style:none}'
|
|
287
|
+
+ 'p{color:#999}@media(prefers-color-scheme:dark){body{background:#111;color:#ddd}a{color:#4d9}}</style>'
|
|
288
|
+
+ `<h1>/${여기}</h1>`
|
|
289
|
+
+ (여기 ? '<li><a href="../">../</a></li>' : '')
|
|
290
|
+
+ 것들.map(줄).join('')
|
|
291
|
+
+ '<p>index.html 이 없어서 목록을 보여 줍니다.</p>';
|
|
292
|
+
const buf = Buffer.from(몸, 'utf8');
|
|
293
|
+
res.writeHead(200, { 'content-type': 'text/html; charset=utf-8', 'content-length': buf.length });
|
|
294
|
+
res.end(buf);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* 기본 브라우저로 연다.
|
|
299
|
+
*
|
|
300
|
+
* 실패해도 조용히 넘어간다 — 주소는 이미 화면에 찍혀 있으므로 손으로 열면 된다.
|
|
301
|
+
* 여기서 오류를 띄우면 '띄우기는 됐는데 실패한 것처럼' 보인다.
|
|
302
|
+
*/
|
|
303
|
+
export function 브라우저로(url) {
|
|
304
|
+
/*
|
|
305
|
+
* 열면 안 되는 자리가 있다.
|
|
306
|
+
*
|
|
307
|
+
* 검사가 돌 때마다 진짜 브라우저 창이 뜨면 사람 화면이 난장판이 된다.
|
|
308
|
+
* 파이프로 넘길 때(로그·캡처)도 열 이유가 없다 — 볼 사람이 없다.
|
|
309
|
+
* 상자 검사는 isTTY 를 거짓말하게 만들어 자식을 띄우므로, TTY 만 봐서는
|
|
310
|
+
* 못 막는다. 그래서 env 로도 막을 수 있게 뒀고 검사 돌리개가 그걸 켠다.
|
|
311
|
+
*/
|
|
312
|
+
if (process.env.DEEL_NO_OPEN) return false;
|
|
313
|
+
if (!process.stdout.isTTY) return false;
|
|
314
|
+
try {
|
|
315
|
+
const [cmd, args] = process.platform === 'win32'
|
|
316
|
+
// start 는 cmd 안엣말이다. 첫 따옴표 한 쌍은 창 제목 자리라 비워 둬야 한다.
|
|
317
|
+
? ['cmd', ['/d', '/s', '/c', 'start', '', url]]
|
|
318
|
+
: process.platform === 'darwin' ? ['open', [url]] : ['xdg-open', [url]];
|
|
319
|
+
const kid = spawn(cmd, args, { stdio: 'ignore', detached: process.platform !== 'win32', windowsHide: true });
|
|
320
|
+
kid.on('error', () => { /* 없으면 그만 */ });
|
|
321
|
+
kid.unref();
|
|
322
|
+
return true;
|
|
323
|
+
} catch {
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
}
|