claude-remote 0.5.1 → 0.6.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/lib/cli.js +154 -0
- package/lib/hooks.js +93 -0
- package/lib/http-server.js +166 -0
- package/lib/image-upload.js +559 -0
- package/lib/logger.js +216 -0
- package/lib/pty-manager.js +162 -0
- package/lib/state.js +118 -0
- package/lib/transcript.js +535 -0
- package/lib/ws-server.js +494 -0
- package/package.json +4 -2
- package/server.js +6 -0
- package/web/index.html +346 -0
- package/web/main.js +68 -0
- package/web/modules/chat-cache.js +118 -0
- package/web/modules/confirm.js +25 -0
- package/web/modules/constants.js +59 -0
- package/web/modules/debug.js +81 -0
- package/web/modules/dir-picker.js +128 -0
- package/web/modules/hub.js +619 -0
- package/web/modules/image-upload.js +290 -0
- package/web/modules/input.js +279 -0
- package/web/modules/interactions.js +304 -0
- package/web/modules/keyboard.js +78 -0
- package/web/modules/model-picker.js +47 -0
- package/web/modules/permissions.js +94 -0
- package/web/modules/renderer.js +863 -0
- package/web/modules/sessions.js +108 -0
- package/web/modules/settings.js +74 -0
- package/web/modules/state.js +59 -0
- package/web/modules/toast.js +68 -0
- package/web/modules/todo.js +292 -0
- package/web/modules/utils.js +102 -0
- package/web/modules/waiting.js +93 -0
- package/web/modules/websocket.js +483 -0
- package/web/styles.css +1722 -0
package/lib/cli.js
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const crypto = require('crypto');
|
|
5
|
+
const { TOKEN_FILE } = require('./state');
|
|
6
|
+
|
|
7
|
+
// --- CLI argument parsing ---
|
|
8
|
+
const BLOCKED_FLAGS = new Set([
|
|
9
|
+
'--print', '-p',
|
|
10
|
+
'--output-format',
|
|
11
|
+
'--input-format',
|
|
12
|
+
'--include-partial-messages',
|
|
13
|
+
'--json-schema',
|
|
14
|
+
'--no-session-persistence',
|
|
15
|
+
'--max-budget-usd',
|
|
16
|
+
'--max-turns',
|
|
17
|
+
'--fallback-model',
|
|
18
|
+
'--permission-prompt-tool',
|
|
19
|
+
'--version', '-v',
|
|
20
|
+
'--help', '-h',
|
|
21
|
+
'--init-only',
|
|
22
|
+
'--maintenance',
|
|
23
|
+
'--token',
|
|
24
|
+
'--no-auth',
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
const FLAGS_WITH_VALUE = new Set([
|
|
28
|
+
'--resume', '-r', '--session-id', '--from-pr', '--model',
|
|
29
|
+
'--system-prompt', '--system-prompt-file',
|
|
30
|
+
'--append-system-prompt', '--append-system-prompt-file',
|
|
31
|
+
'--permission-mode', '--add-dir', '--worktree', '-w',
|
|
32
|
+
'--mcp-config', '--settings', '--setting-sources',
|
|
33
|
+
'--agent', '--agents', '--teammate-mode',
|
|
34
|
+
'--allowedTools', '--disallowedTools', '--tools',
|
|
35
|
+
'--betas', '--debug', '--plugin-dir',
|
|
36
|
+
'--output-format', '--input-format', '--json-schema',
|
|
37
|
+
'--max-budget-usd', '--max-turns', '--fallback-model',
|
|
38
|
+
'--permission-prompt-tool',
|
|
39
|
+
'--token',
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
function parseCliArgs(argv) {
|
|
43
|
+
const rawArgs = argv.slice(2);
|
|
44
|
+
let cwd = null;
|
|
45
|
+
const claudeArgs = [];
|
|
46
|
+
const blocked = [];
|
|
47
|
+
let token = null;
|
|
48
|
+
let noAuth = false;
|
|
49
|
+
|
|
50
|
+
let i = 0;
|
|
51
|
+
while (i < rawArgs.length) {
|
|
52
|
+
const arg = rawArgs[i];
|
|
53
|
+
|
|
54
|
+
if (arg === '--') {
|
|
55
|
+
claudeArgs.push(...rawArgs.slice(i + 1));
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!arg.startsWith('-')) {
|
|
60
|
+
if (!cwd) {
|
|
61
|
+
cwd = arg;
|
|
62
|
+
} else {
|
|
63
|
+
claudeArgs.push(arg);
|
|
64
|
+
}
|
|
65
|
+
i++;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const eqIdx = arg.indexOf('=');
|
|
70
|
+
const flagName = eqIdx > 0 ? arg.substring(0, eqIdx) : arg;
|
|
71
|
+
|
|
72
|
+
if (flagName === '--token') {
|
|
73
|
+
if (eqIdx > 0) {
|
|
74
|
+
token = arg.substring(eqIdx + 1);
|
|
75
|
+
} else if (i + 1 < rawArgs.length && !rawArgs[i + 1].startsWith('-')) {
|
|
76
|
+
i++;
|
|
77
|
+
token = rawArgs[i];
|
|
78
|
+
} else {
|
|
79
|
+
token = '';
|
|
80
|
+
}
|
|
81
|
+
i++;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (flagName === '--no-auth') {
|
|
85
|
+
noAuth = true;
|
|
86
|
+
i++;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (BLOCKED_FLAGS.has(flagName)) {
|
|
91
|
+
blocked.push(flagName);
|
|
92
|
+
if (eqIdx > 0) {
|
|
93
|
+
// --flag=value, already consumed
|
|
94
|
+
} else if (FLAGS_WITH_VALUE.has(flagName) && i + 1 < rawArgs.length) {
|
|
95
|
+
i++;
|
|
96
|
+
}
|
|
97
|
+
i++;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
claudeArgs.push(arg);
|
|
102
|
+
if (eqIdx < 0 && FLAGS_WITH_VALUE.has(flagName) && i + 1 < rawArgs.length && !rawArgs[i + 1].startsWith('-')) {
|
|
103
|
+
i++;
|
|
104
|
+
claudeArgs.push(rawArgs[i]);
|
|
105
|
+
}
|
|
106
|
+
i++;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return { cwd: cwd || process.cwd(), claudeArgs, blocked, token, noAuth };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// --- Auth token resolution ---
|
|
113
|
+
const AUTH_TOKEN_ENV_VAR = 'CLAUDE_REMOTE_TOKEN';
|
|
114
|
+
const LEGACY_AUTH_TOKEN_ENV_VAR = 'TOKEN';
|
|
115
|
+
|
|
116
|
+
function resolveAuthToken(cliToken, authDisabled) {
|
|
117
|
+
if (authDisabled) return null;
|
|
118
|
+
if (cliToken) return cliToken;
|
|
119
|
+
if (process.env[AUTH_TOKEN_ENV_VAR]) return process.env[AUTH_TOKEN_ENV_VAR];
|
|
120
|
+
try {
|
|
121
|
+
const saved = fs.readFileSync(TOKEN_FILE, 'utf8').trim();
|
|
122
|
+
if (saved) return saved;
|
|
123
|
+
} catch {}
|
|
124
|
+
const generated = crypto.randomBytes(24).toString('base64url');
|
|
125
|
+
try { fs.writeFileSync(TOKEN_FILE, generated + '\n', { mode: 0o600 }); } catch {}
|
|
126
|
+
return generated;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function initConfig() {
|
|
130
|
+
const parsed = parseCliArgs(process.argv);
|
|
131
|
+
const authDisabled = parsed.noAuth || process.env.NO_AUTH === '1';
|
|
132
|
+
const authToken = resolveAuthToken(parsed.token, authDisabled);
|
|
133
|
+
const unusedLegacyTokenEnv = !!process.env[LEGACY_AUTH_TOKEN_ENV_VAR] && !process.env[AUTH_TOKEN_ENV_VAR];
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
PORT: parseInt(process.env.PORT || '3100', 10),
|
|
137
|
+
CWD: parsed.cwd,
|
|
138
|
+
AUTH_TOKEN: authToken,
|
|
139
|
+
AUTH_DISABLED: authDisabled,
|
|
140
|
+
ENABLE_WEB: process.env.ENABLE_WEB === '1',
|
|
141
|
+
CLAUDE_EXTRA_ARGS: parsed.claudeArgs,
|
|
142
|
+
DEBUG_TTY_INPUT: process.env.CLAUDE_REMOTE_DEBUG_TTY_INPUT === '1',
|
|
143
|
+
blockedArgs: parsed.blocked,
|
|
144
|
+
unusedLegacyTokenEnv,
|
|
145
|
+
LEGACY_AUTH_TOKEN_ENV_VAR,
|
|
146
|
+
AUTH_TOKEN_ENV_VAR,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
module.exports = {
|
|
151
|
+
parseCliArgs,
|
|
152
|
+
resolveAuthToken,
|
|
153
|
+
initConfig,
|
|
154
|
+
};
|
package/lib/hooks.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { state } = require('./state');
|
|
6
|
+
const { log } = require('./logger');
|
|
7
|
+
|
|
8
|
+
function setupHooks() {
|
|
9
|
+
const claudeDir = path.join(state.CWD, '.claude');
|
|
10
|
+
if (!fs.existsSync(claudeDir)) fs.mkdirSync(claudeDir, { recursive: true });
|
|
11
|
+
|
|
12
|
+
const settingsPath = path.join(claudeDir, 'settings.local.json');
|
|
13
|
+
let settings = {};
|
|
14
|
+
try { settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); } catch {}
|
|
15
|
+
|
|
16
|
+
const hookScript = path.resolve(__dirname, '..', 'hooks', 'bridge-approval.js').replace(/\\/g, '/');
|
|
17
|
+
const hookCmd = `node "${hookScript}"`;
|
|
18
|
+
|
|
19
|
+
// Merge bridge hook into PreToolUse (preserve user's other hooks)
|
|
20
|
+
const existing = settings.hooks?.PreToolUse || [];
|
|
21
|
+
const bridgeIdx = existing.findIndex(e =>
|
|
22
|
+
e.hooks?.some(h => h.command?.includes('bridge-approval'))
|
|
23
|
+
);
|
|
24
|
+
const bridgeEntry = {
|
|
25
|
+
matcher: '',
|
|
26
|
+
hooks: [{ type: 'command', command: hookCmd, timeout: 120 }],
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
if (bridgeIdx >= 0) {
|
|
30
|
+
existing[bridgeIdx] = bridgeEntry;
|
|
31
|
+
} else {
|
|
32
|
+
existing.push(bridgeEntry);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
settings.hooks = settings.hooks || {};
|
|
36
|
+
settings.hooks.PreToolUse = existing;
|
|
37
|
+
|
|
38
|
+
// Merge bridge hook into Stop
|
|
39
|
+
const stopScript = path.resolve(__dirname, '..', 'hooks', 'bridge-stop.js').replace(/\\/g, '/');
|
|
40
|
+
const stopCmd = `node "${stopScript}"`;
|
|
41
|
+
const existingStop = settings.hooks.Stop || [];
|
|
42
|
+
const stopBridgeIdx = existingStop.findIndex(e =>
|
|
43
|
+
e.hooks?.some(h => h.command?.includes('bridge-stop'))
|
|
44
|
+
);
|
|
45
|
+
const stopEntry = {
|
|
46
|
+
hooks: [{ type: 'command', command: stopCmd, timeout: 10 }],
|
|
47
|
+
};
|
|
48
|
+
if (stopBridgeIdx >= 0) {
|
|
49
|
+
existingStop[stopBridgeIdx] = stopEntry;
|
|
50
|
+
} else {
|
|
51
|
+
existingStop.push(stopEntry);
|
|
52
|
+
}
|
|
53
|
+
settings.hooks.Stop = existingStop;
|
|
54
|
+
|
|
55
|
+
// SessionStart
|
|
56
|
+
const sessionStartScript = path.resolve(__dirname, '..', 'hooks', 'bridge-session-start.js').replace(/\\/g, '/');
|
|
57
|
+
const sessionStartCmd = `node "${sessionStartScript}"`;
|
|
58
|
+
const existingSessionStart = settings.hooks.SessionStart || [];
|
|
59
|
+
const sessionStartBridgeIdx = existingSessionStart.findIndex(e =>
|
|
60
|
+
e.hooks?.some(h => h.command?.includes('bridge-session-start'))
|
|
61
|
+
);
|
|
62
|
+
const sessionStartEntry = {
|
|
63
|
+
hooks: [{ type: 'command', command: sessionStartCmd, timeout: 10 }],
|
|
64
|
+
};
|
|
65
|
+
if (sessionStartBridgeIdx >= 0) {
|
|
66
|
+
existingSessionStart[sessionStartBridgeIdx] = sessionStartEntry;
|
|
67
|
+
} else {
|
|
68
|
+
existingSessionStart.push(sessionStartEntry);
|
|
69
|
+
}
|
|
70
|
+
settings.hooks.SessionStart = existingSessionStart;
|
|
71
|
+
|
|
72
|
+
// SessionEnd
|
|
73
|
+
const sessionEndScript = path.resolve(__dirname, '..', 'hooks', 'bridge-session-end.js').replace(/\\/g, '/');
|
|
74
|
+
const sessionEndCmd = `node "${sessionEndScript}"`;
|
|
75
|
+
const existingSessionEnd = settings.hooks.SessionEnd || [];
|
|
76
|
+
const sessionEndBridgeIdx = existingSessionEnd.findIndex(e =>
|
|
77
|
+
e.hooks?.some(h => h.command?.includes('bridge-session-end'))
|
|
78
|
+
);
|
|
79
|
+
const sessionEndEntry = {
|
|
80
|
+
hooks: [{ type: 'command', command: sessionEndCmd, timeout: 10 }],
|
|
81
|
+
};
|
|
82
|
+
if (sessionEndBridgeIdx >= 0) {
|
|
83
|
+
existingSessionEnd[sessionEndBridgeIdx] = sessionEndEntry;
|
|
84
|
+
} else {
|
|
85
|
+
existingSessionEnd.push(sessionEndEntry);
|
|
86
|
+
}
|
|
87
|
+
settings.hooks.SessionEnd = existingSessionEnd;
|
|
88
|
+
|
|
89
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
90
|
+
log(`Hooks configured: ${settingsPath}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
module.exports = { setupHooks };
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const http = require('http');
|
|
6
|
+
const { state, ALWAYS_AUTO_ALLOW, PARTIAL_AUTO_ALLOW } = require('./state');
|
|
7
|
+
const { log, broadcast, isAuthenticatedClient, setTurnState, recomputeEffectiveApprovalMode } = require('./logger');
|
|
8
|
+
const { maybeAttachHookSession, markExpectingSwitch } = require('./transcript');
|
|
9
|
+
|
|
10
|
+
const MIME = {
|
|
11
|
+
'.html': 'text/html; charset=utf-8',
|
|
12
|
+
'.js': 'text/javascript; charset=utf-8',
|
|
13
|
+
'.css': 'text/css; charset=utf-8',
|
|
14
|
+
'.json': 'application/json',
|
|
15
|
+
'.png': 'image/png',
|
|
16
|
+
'.svg': 'image/svg+xml',
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function createHttpServer() {
|
|
20
|
+
return http.createServer((req, res) => {
|
|
21
|
+
const url = req.url.split('?')[0];
|
|
22
|
+
|
|
23
|
+
// --- API: Hook approval endpoint ---
|
|
24
|
+
if (req.method === 'POST' && url === '/hook/pre-tool-use') {
|
|
25
|
+
let body = '';
|
|
26
|
+
req.on('data', chunk => (body += chunk));
|
|
27
|
+
req.on('end', () => {
|
|
28
|
+
let data;
|
|
29
|
+
try { data = JSON.parse(body); } catch {
|
|
30
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
31
|
+
res.end(JSON.stringify({ decision: 'ask' }));
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
maybeAttachHookSession(data, 'pre-tool-use');
|
|
36
|
+
const effectiveApprovalMode = recomputeEffectiveApprovalMode('pre-tool-use');
|
|
37
|
+
|
|
38
|
+
if (ALWAYS_AUTO_ALLOW.has(data.tool_name)) {
|
|
39
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
40
|
+
res.end(JSON.stringify({ decision: 'allow' }));
|
|
41
|
+
log(`Permission auto-allowed (always): ${data.tool_name}`);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (effectiveApprovalMode === 'all') {
|
|
46
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
47
|
+
res.end(JSON.stringify({ decision: 'allow' }));
|
|
48
|
+
log(`Permission auto-allowed (mode=all): ${data.tool_name}`);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (effectiveApprovalMode === 'partial' && PARTIAL_AUTO_ALLOW.has(data.tool_name)) {
|
|
52
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
53
|
+
res.end(JSON.stringify({ decision: 'allow' }));
|
|
54
|
+
log(`Permission auto-allowed (mode=partial): ${data.tool_name}`);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const clients = [...state.wss.clients].filter(isAuthenticatedClient);
|
|
59
|
+
if (clients.length === 0) {
|
|
60
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
61
|
+
res.end(JSON.stringify({ decision: 'ask' }));
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const id = String(++state.approvalSeq);
|
|
66
|
+
log(`Permission #${id}: ${data.tool_name} → ${clients.length} WebUI client(s)`);
|
|
67
|
+
|
|
68
|
+
broadcast({
|
|
69
|
+
type: 'permission_request',
|
|
70
|
+
id,
|
|
71
|
+
toolName: data.tool_name,
|
|
72
|
+
toolInput: data.tool_input,
|
|
73
|
+
permissionMode: data.permission_mode,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const timer = setTimeout(() => {
|
|
77
|
+
state.pendingApprovals.delete(id);
|
|
78
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
79
|
+
res.end(JSON.stringify({ decision: 'ask' }));
|
|
80
|
+
log(`Permission #${id}: timeout → ask`);
|
|
81
|
+
broadcast({
|
|
82
|
+
type: 'permission_resolved',
|
|
83
|
+
id,
|
|
84
|
+
decision: 'ask',
|
|
85
|
+
reason: 'timeout',
|
|
86
|
+
});
|
|
87
|
+
}, 90000);
|
|
88
|
+
|
|
89
|
+
state.pendingApprovals.set(id, { res, timer });
|
|
90
|
+
});
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// --- API: Session start hook endpoint ---
|
|
95
|
+
if (req.method === 'POST' && url === '/hook/session-start') {
|
|
96
|
+
let body = '';
|
|
97
|
+
req.on('data', chunk => (body += chunk));
|
|
98
|
+
req.on('end', () => {
|
|
99
|
+
try {
|
|
100
|
+
const data = JSON.parse(body);
|
|
101
|
+
log(`/hook/session-start received (source=${data.source || 'unknown'}, session_id=${data.session_id || 'none'})`);
|
|
102
|
+
maybeAttachHookSession(data, 'session-start');
|
|
103
|
+
} catch {}
|
|
104
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
105
|
+
res.end('{}');
|
|
106
|
+
});
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// --- API: Session end hook endpoint ---
|
|
111
|
+
if (req.method === 'POST' && url === '/hook/session-end') {
|
|
112
|
+
let body = '';
|
|
113
|
+
req.on('data', chunk => (body += chunk));
|
|
114
|
+
req.on('end', () => {
|
|
115
|
+
let data = {};
|
|
116
|
+
try { data = JSON.parse(body); } catch {}
|
|
117
|
+
const reason = data.reason || 'unknown';
|
|
118
|
+
log(`/hook/session-end received (reason=${reason})`);
|
|
119
|
+
if (reason === 'clear') {
|
|
120
|
+
markExpectingSwitch();
|
|
121
|
+
}
|
|
122
|
+
setTurnState('idle', { reason: `session-end:${reason}` });
|
|
123
|
+
broadcast({ type: 'session_end', reason });
|
|
124
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
125
|
+
res.end('{}');
|
|
126
|
+
});
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// --- API: Stop hook endpoint ---
|
|
131
|
+
if (req.method === 'POST' && url === '/hook/stop') {
|
|
132
|
+
let body = '';
|
|
133
|
+
req.on('data', chunk => (body += chunk));
|
|
134
|
+
req.on('end', () => {
|
|
135
|
+
log('/hook/stop received — broadcasting turn_complete');
|
|
136
|
+
try {
|
|
137
|
+
maybeAttachHookSession(JSON.parse(body), 'stop');
|
|
138
|
+
} catch {}
|
|
139
|
+
setTurnState('idle', { reason: 'stop-hook' });
|
|
140
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
141
|
+
res.end('{}');
|
|
142
|
+
});
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// --- Static files ---
|
|
147
|
+
if (!state.ENABLE_WEB) {
|
|
148
|
+
res.writeHead(403, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
149
|
+
res.end('Web UI disabled. Start with ENABLE_WEB=1 to enable.');
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const filePath = path.join(__dirname, '..', 'web', url === '/' ? 'index.html' : url);
|
|
153
|
+
const ext = path.extname(filePath);
|
|
154
|
+
fs.readFile(filePath, (err, data) => {
|
|
155
|
+
if (err) {
|
|
156
|
+
res.writeHead(404);
|
|
157
|
+
res.end('Not found');
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
res.writeHead(200, { 'Content-Type': MIME[ext] || 'application/octet-stream' });
|
|
161
|
+
res.end(data);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
module.exports = { createHttpServer };
|