nterminal 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/.env.example +12 -0
- package/LICENSE +674 -0
- package/README.md +181 -0
- package/assets/brand/app-icon-1024.png +0 -0
- package/assets/brand/app-icon-384.png +0 -0
- package/assets/brand/apple-touch-icon-360.png +0 -0
- package/assets/brand/favicon-32.png +0 -0
- package/assets/brand/favicon-64.png +0 -0
- package/assets/brand/favicon-96.png +0 -0
- package/assets/brand/favicon.svg +4 -0
- package/assets/brand/nterminal-mark-64.png +0 -0
- package/assets/brand/nterminal-mark.svg +4 -0
- package/assets/brand/nterminal-wordmark-486x68.png +0 -0
- package/assets/brand/nterminal-wordmark.svg +3 -0
- package/assets/screenshot/scr.png +0 -0
- package/bin/nterminal.js +114 -0
- package/dist/client/apple-touch-icon.png +0 -0
- package/dist/client/assets/MarkdownPreview-BeDi-V7k.js +29 -0
- package/dist/client/assets/MesloLGS-NF-Bold-Italic-DwFsXcwX.ttf +0 -0
- package/dist/client/assets/MesloLGS-NF-Bold-kN-HYz-g.ttf +0 -0
- package/dist/client/assets/MesloLGS-NF-Italic-CMg1T6-G.ttf +0 -0
- package/dist/client/assets/MesloLGS-NF-Regular-Cxr8pvCI.ttf +0 -0
- package/dist/client/assets/index-BQkKYjXb.js +33 -0
- package/dist/client/assets/index-WqeS39wU.css +1 -0
- package/dist/client/assets/notifications/character-2258.mp4 +0 -0
- package/dist/client/assets/notifications/character-2260.mp4 +0 -0
- package/dist/client/assets/notifications/character-2272.mp4 +0 -0
- package/dist/client/brand/nterminal-mark-64.png +0 -0
- package/dist/client/brand/nterminal-mark.svg +4 -0
- package/dist/client/brand/nterminal-wordmark-486x68.png +0 -0
- package/dist/client/brand/nterminal-wordmark.svg +3 -0
- package/dist/client/icons/app-icon-1024.png +0 -0
- package/dist/client/icons/app-icon-384.png +0 -0
- package/dist/client/icons/favicon-32.png +0 -0
- package/dist/client/icons/favicon-64.png +0 -0
- package/dist/client/icons/favicon-96.png +0 -0
- package/dist/client/icons/favicon.svg +4 -0
- package/dist/client/index.html +21 -0
- package/dist/client/manifest.webmanifest +24 -0
- package/dist/scripts/generate-secrets.js +3 -0
- package/dist/scripts/generate-secrets.js.map +1 -0
- package/dist/scripts/onboarding.js +814 -0
- package/dist/scripts/onboarding.js.map +1 -0
- package/dist/scripts/proxySetup.js +1007 -0
- package/dist/scripts/proxySetup.js.map +1 -0
- package/dist/server/agent/agentAuth.d.ts +6 -0
- package/dist/server/agent/agentAuth.js +35 -0
- package/dist/server/agent/agentAuth.js.map +1 -0
- package/dist/server/agent/agentProxy.d.ts +5 -0
- package/dist/server/agent/agentProxy.js +63 -0
- package/dist/server/agent/agentProxy.js.map +1 -0
- package/dist/server/agent/agentRoutes.d.ts +9 -0
- package/dist/server/agent/agentRoutes.js +327 -0
- package/dist/server/agent/agentRoutes.js.map +1 -0
- package/dist/server/agent/agentWebSocketProxy.d.ts +3 -0
- package/dist/server/agent/agentWebSocketProxy.js +65 -0
- package/dist/server/agent/agentWebSocketProxy.js.map +1 -0
- package/dist/server/auth/authService.d.ts +100 -0
- package/dist/server/auth/authService.js +415 -0
- package/dist/server/auth/authService.js.map +1 -0
- package/dist/server/auth/cookies.d.ts +11 -0
- package/dist/server/auth/cookies.js +39 -0
- package/dist/server/auth/cookies.js.map +1 -0
- package/dist/server/auth/ipMatch.d.ts +14 -0
- package/dist/server/auth/ipMatch.js +103 -0
- package/dist/server/auth/ipMatch.js.map +1 -0
- package/dist/server/auth/rateLimit.d.ts +17 -0
- package/dist/server/auth/rateLimit.js +25 -0
- package/dist/server/auth/rateLimit.js.map +1 -0
- package/dist/server/auth/totpService.d.ts +10 -0
- package/dist/server/auth/totpService.js +37 -0
- package/dist/server/auth/totpService.js.map +1 -0
- package/dist/server/config.d.ts +27 -0
- package/dist/server/config.js +138 -0
- package/dist/server/config.js.map +1 -0
- package/dist/server/files/fileExplorerService.d.ts +38 -0
- package/dist/server/files/fileExplorerService.js +551 -0
- package/dist/server/files/fileExplorerService.js.map +1 -0
- package/dist/server/files/rootToken.d.ts +51 -0
- package/dist/server/files/rootToken.js +139 -0
- package/dist/server/files/rootToken.js.map +1 -0
- package/dist/server/http.d.ts +13 -0
- package/dist/server/http.js +69 -0
- package/dist/server/http.js.map +1 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +45 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/routes/agentManagementRoutes.d.ts +9 -0
- package/dist/server/routes/agentManagementRoutes.js +304 -0
- package/dist/server/routes/agentManagementRoutes.js.map +1 -0
- package/dist/server/routes/authRoutes.d.ts +10 -0
- package/dist/server/routes/authRoutes.js +95 -0
- package/dist/server/routes/authRoutes.js.map +1 -0
- package/dist/server/routes/fileRoutes.d.ts +11 -0
- package/dist/server/routes/fileRoutes.js +185 -0
- package/dist/server/routes/fileRoutes.js.map +1 -0
- package/dist/server/routes/notificationAssetRoutes.d.ts +9 -0
- package/dist/server/routes/notificationAssetRoutes.js +280 -0
- package/dist/server/routes/notificationAssetRoutes.js.map +1 -0
- package/dist/server/routes/securityRoutes.d.ts +7 -0
- package/dist/server/routes/securityRoutes.js +53 -0
- package/dist/server/routes/securityRoutes.js.map +1 -0
- package/dist/server/routes/socketBackpressure.d.ts +26 -0
- package/dist/server/routes/socketBackpressure.js +63 -0
- package/dist/server/routes/socketBackpressure.js.map +1 -0
- package/dist/server/routes/terminalLayoutRoutes.d.ts +9 -0
- package/dist/server/routes/terminalLayoutRoutes.js +108 -0
- package/dist/server/routes/terminalLayoutRoutes.js.map +1 -0
- package/dist/server/routes/terminalRoutes.d.ts +14 -0
- package/dist/server/routes/terminalRoutes.js +177 -0
- package/dist/server/routes/terminalRoutes.js.map +1 -0
- package/dist/server/routes/terminalWebSocket.d.ts +9 -0
- package/dist/server/routes/terminalWebSocket.js +129 -0
- package/dist/server/routes/terminalWebSocket.js.map +1 -0
- package/dist/server/routes/totpRoutes.d.ts +7 -0
- package/dist/server/routes/totpRoutes.js +46 -0
- package/dist/server/routes/totpRoutes.js.map +1 -0
- package/dist/server/routes/updateRoutes.d.ts +7 -0
- package/dist/server/routes/updateRoutes.js +24 -0
- package/dist/server/routes/updateRoutes.js.map +1 -0
- package/dist/server/routes/uploadRoutes.d.ts +9 -0
- package/dist/server/routes/uploadRoutes.js +95 -0
- package/dist/server/routes/uploadRoutes.js.map +1 -0
- package/dist/server/storage/fileStore.d.ts +90 -0
- package/dist/server/storage/fileStore.js +275 -0
- package/dist/server/storage/fileStore.js.map +1 -0
- package/dist/server/system/stats.d.ts +2 -0
- package/dist/server/system/stats.js +37 -0
- package/dist/server/system/stats.js.map +1 -0
- package/dist/server/terminal/NodePtyAdapter.d.ts +4 -0
- package/dist/server/terminal/NodePtyAdapter.js +14 -0
- package/dist/server/terminal/NodePtyAdapter.js.map +1 -0
- package/dist/server/terminal/PtyAdapter.d.ts +57 -0
- package/dist/server/terminal/PtyAdapter.js +2 -0
- package/dist/server/terminal/PtyAdapter.js.map +1 -0
- package/dist/server/terminal/TerminalManager.d.ts +74 -0
- package/dist/server/terminal/TerminalManager.js +561 -0
- package/dist/server/terminal/TerminalManager.js.map +1 -0
- package/dist/server/terminal/TmuxPtyAdapter.d.ts +25 -0
- package/dist/server/terminal/TmuxPtyAdapter.js +543 -0
- package/dist/server/terminal/TmuxPtyAdapter.js.map +1 -0
- package/dist/server/terminal/codexTranscriptSource.d.ts +9 -0
- package/dist/server/terminal/codexTranscriptSource.js +144 -0
- package/dist/server/terminal/codexTranscriptSource.js.map +1 -0
- package/dist/server/terminal/cwdResolver.d.ts +8 -0
- package/dist/server/terminal/cwdResolver.js +37 -0
- package/dist/server/terminal/cwdResolver.js.map +1 -0
- package/dist/server/terminal/outputBuffer.d.ts +7 -0
- package/dist/server/terminal/outputBuffer.js +17 -0
- package/dist/server/terminal/outputBuffer.js.map +1 -0
- package/dist/server/terminal/transcriptHistory.d.ts +7 -0
- package/dist/server/terminal/transcriptHistory.js +315 -0
- package/dist/server/terminal/transcriptHistory.js.map +1 -0
- package/dist/server/update/gitUpdate.d.ts +27 -0
- package/dist/server/update/gitUpdate.js +241 -0
- package/dist/server/update/gitUpdate.js.map +1 -0
- package/dist/server/uploads/uploadPaths.d.ts +18 -0
- package/dist/server/uploads/uploadPaths.js +116 -0
- package/dist/server/uploads/uploadPaths.js.map +1 -0
- package/dist/server/uploads/uploadService.d.ts +21 -0
- package/dist/server/uploads/uploadService.js +230 -0
- package/dist/server/uploads/uploadService.js.map +1 -0
- package/dist/shared/layoutState.d.ts +6 -0
- package/dist/shared/layoutState.js +115 -0
- package/dist/shared/layoutState.js.map +1 -0
- package/dist/shared/notificationAssets.d.ts +9 -0
- package/dist/shared/notificationAssets.js +27 -0
- package/dist/shared/notificationAssets.js.map +1 -0
- package/dist/shared/protocol.d.ts +308 -0
- package/dist/shared/protocol.js +29 -0
- package/dist/shared/protocol.js.map +1 -0
- package/dist/shared/types.d.ts +56 -0
- package/dist/shared/types.js +2 -0
- package/dist/shared/types.js.map +1 -0
- package/docs/assets/nterminal-workspace.png +0 -0
- package/docs/configuration.md +97 -0
- package/docs/features.md +126 -0
- package/docs/onboarding.md +122 -0
- package/docs/operations.md +112 -0
- package/docs/terminal-history.md +54 -0
- package/package.json +85 -0
- package/public/apple-touch-icon.png +0 -0
- package/public/assets/notifications/character-2258.mp4 +0 -0
- package/public/assets/notifications/character-2260.mp4 +0 -0
- package/public/assets/notifications/character-2272.mp4 +0 -0
- package/public/brand/nterminal-mark-64.png +0 -0
- package/public/brand/nterminal-mark.svg +4 -0
- package/public/brand/nterminal-wordmark-486x68.png +0 -0
- package/public/brand/nterminal-wordmark.svg +3 -0
- package/public/icons/app-icon-1024.png +0 -0
- package/public/icons/app-icon-384.png +0 -0
- package/public/icons/favicon-32.png +0 -0
- package/public/icons/favicon-64.png +0 -0
- package/public/icons/favicon-96.png +0 -0
- package/public/icons/favicon.svg +4 -0
- package/public/manifest.webmanifest +24 -0
- package/scripts/nterminalctl +588 -0
|
@@ -0,0 +1,588 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -Eeuo pipefail
|
|
3
|
+
|
|
4
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
5
|
+
DEFAULT_APP_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
6
|
+
APP_DIR="${NTERMINAL_APP_DIR:-$DEFAULT_APP_DIR}"
|
|
7
|
+
ENV_FILE="${NTERMINAL_ENV_FILE:-$APP_DIR/.env}"
|
|
8
|
+
|
|
9
|
+
COMMAND="${1:-help}"
|
|
10
|
+
if [[ $# -gt 0 ]]; then
|
|
11
|
+
shift
|
|
12
|
+
fi
|
|
13
|
+
|
|
14
|
+
die() {
|
|
15
|
+
echo "error: $*" >&2
|
|
16
|
+
exit 1
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
info() {
|
|
20
|
+
echo "$*"
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
trim() {
|
|
24
|
+
local value="$1"
|
|
25
|
+
value="${value#"${value%%[![:space:]]*}"}"
|
|
26
|
+
value="${value%"${value##*[![:space:]]}"}"
|
|
27
|
+
printf '%s' "$value"
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
strip_quotes() {
|
|
31
|
+
local value="$1"
|
|
32
|
+
if [[ "$value" == \"*\" && "$value" == *\" ]]; then
|
|
33
|
+
value="${value:1:${#value}-2}"
|
|
34
|
+
elif [[ "$value" == \'*\' && "$value" == *\' ]]; then
|
|
35
|
+
value="${value:1:${#value}-2}"
|
|
36
|
+
fi
|
|
37
|
+
printf '%s' "$value"
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
load_env_file() {
|
|
41
|
+
[[ -f "$ENV_FILE" ]] || return 0
|
|
42
|
+
local line key value
|
|
43
|
+
while IFS= read -r line || [[ -n "$line" ]]; do
|
|
44
|
+
line="$(trim "$line")"
|
|
45
|
+
[[ -z "$line" || "$line" == \#* ]] && continue
|
|
46
|
+
[[ "$line" == export\ * ]] && line="$(trim "${line#export }")"
|
|
47
|
+
[[ "$line" == *=* ]] || continue
|
|
48
|
+
key="$(trim "${line%%=*}")"
|
|
49
|
+
value="$(strip_quotes "$(trim "${line#*=}")")"
|
|
50
|
+
[[ "$key" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]] || continue
|
|
51
|
+
if [[ -z "${!key+x}" ]]; then
|
|
52
|
+
export "$key=$value"
|
|
53
|
+
fi
|
|
54
|
+
done < "$ENV_FILE"
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
resolve_path() {
|
|
58
|
+
local value="$1"
|
|
59
|
+
if [[ "$value" == /* ]]; then
|
|
60
|
+
printf '%s' "$value"
|
|
61
|
+
else
|
|
62
|
+
printf '%s/%s' "$APP_DIR" "$value"
|
|
63
|
+
fi
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
load_env_file
|
|
67
|
+
|
|
68
|
+
HOST="${NTERMINAL_HOST:-127.0.0.1}"
|
|
69
|
+
PORT="${NTERMINAL_PORT:-3107}"
|
|
70
|
+
NODE_BIN="${NTERMINAL_NODE_BIN:-node}"
|
|
71
|
+
NPM_BIN="${NTERMINAL_NPM_BIN:-npm}"
|
|
72
|
+
PID_PATH="$(resolve_path "${NTERMINAL_PID_PATH:-.nterminal/nterminal.pid}")"
|
|
73
|
+
LOG_PATH="$(resolve_path "${NTERMINAL_LOG_PATH:-.nterminal/nterminal.log}")"
|
|
74
|
+
STATE_PATH="$(resolve_path "${NTERMINAL_STATE_PATH:-.nterminal/state.json}")"
|
|
75
|
+
HEALTH_TIMEOUT_SECONDS="${NTERMINAL_HEALTH_TIMEOUT_SECONDS:-15}"
|
|
76
|
+
STOP_TIMEOUT_SECONDS="${NTERMINAL_STOP_TIMEOUT_SECONDS:-20}"
|
|
77
|
+
RUNTIME_ENTRY="$APP_DIR/dist/server/index.js"
|
|
78
|
+
LAUNCHD_LABEL="${NTERMINAL_LAUNCHD_LABEL:-com.nterminal.local}"
|
|
79
|
+
UPDATE_LOCK_PATH="${NTERMINAL_UPDATE_LOCK_PATH:-}"
|
|
80
|
+
if [[ -n "$UPDATE_LOCK_PATH" && "$UPDATE_LOCK_PATH" != /* ]]; then
|
|
81
|
+
UPDATE_LOCK_PATH="$(resolve_path "$UPDATE_LOCK_PATH")"
|
|
82
|
+
fi
|
|
83
|
+
|
|
84
|
+
cleanup_update_lock() {
|
|
85
|
+
if [[ -n "$UPDATE_LOCK_PATH" ]]; then
|
|
86
|
+
rm -f "$UPDATE_LOCK_PATH"
|
|
87
|
+
fi
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if [[ -n "$UPDATE_LOCK_PATH" ]]; then
|
|
91
|
+
trap cleanup_update_lock EXIT
|
|
92
|
+
fi
|
|
93
|
+
|
|
94
|
+
health_url() {
|
|
95
|
+
printf 'http://%s:%s/api/auth/session' "$HOST" "$PORT"
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
public_url() {
|
|
99
|
+
printf 'http://%s:%s' "$HOST" "$PORT"
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
ensure_app_dir() {
|
|
103
|
+
[[ -d "$APP_DIR" ]] || die "app directory not found: $APP_DIR"
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
ensure_env_file() {
|
|
107
|
+
[[ -f "$ENV_FILE" ]] || die ".env not found at $ENV_FILE; copy .env.example and run npm run generate:secrets"
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
ensure_runtime() {
|
|
111
|
+
[[ -f "$RUNTIME_ENTRY" ]] || die "dist/server/index.js not found; run scripts/nterminalctl build first"
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
ensure_directories() {
|
|
115
|
+
mkdir -p "$(dirname "$PID_PATH")" "$(dirname "$LOG_PATH")"
|
|
116
|
+
: >> "$LOG_PATH"
|
|
117
|
+
chmod 600 "$LOG_PATH" 2>/dev/null || true
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
validate_required_env() {
|
|
121
|
+
local missing=0
|
|
122
|
+
for key in NTERMINAL_SESSION_SECRET; do
|
|
123
|
+
local value="${!key:-}"
|
|
124
|
+
if [[ -z "$value" || "$value" == replace-with-generate-secrets-output ]]; then
|
|
125
|
+
echo "missing or placeholder env: $key"
|
|
126
|
+
missing=1
|
|
127
|
+
fi
|
|
128
|
+
done
|
|
129
|
+
return "$missing"
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
read_pid() {
|
|
133
|
+
[[ -f "$PID_PATH" ]] || return 1
|
|
134
|
+
read_pid_file "$PID_PATH"
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
read_pid_file() {
|
|
138
|
+
local pid_path="$1"
|
|
139
|
+
[[ -f "$pid_path" ]] || return 1
|
|
140
|
+
local pid
|
|
141
|
+
pid="$(tr -d '[:space:]' < "$pid_path")"
|
|
142
|
+
[[ "$pid" =~ ^[0-9]+$ ]] || return 1
|
|
143
|
+
printf '%s' "$pid"
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
is_running() {
|
|
147
|
+
local pid="$1"
|
|
148
|
+
kill -0 "$pid" 2>/dev/null
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
current_pid() {
|
|
152
|
+
local pid
|
|
153
|
+
pid="$(read_pid || true)"
|
|
154
|
+
if [[ -n "$pid" && "$(is_running "$pid" && echo yes || echo no)" == yes ]]; then
|
|
155
|
+
printf '%s' "$pid"
|
|
156
|
+
return 0
|
|
157
|
+
fi
|
|
158
|
+
return 1
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
launchd_domain() {
|
|
162
|
+
[[ -n "$LAUNCHD_LABEL" ]] || return 1
|
|
163
|
+
printf 'gui/%s/%s' "$(id -u)" "$LAUNCHD_LABEL"
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
launchd_print() {
|
|
167
|
+
command -v launchctl >/dev/null 2>&1 || return 1
|
|
168
|
+
local domain
|
|
169
|
+
domain="$(launchd_domain)" || return 1
|
|
170
|
+
launchctl print "$domain" 2>/dev/null
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
launchd_controls_app() {
|
|
174
|
+
local output
|
|
175
|
+
output="$(launchd_print || true)"
|
|
176
|
+
[[ -n "$output" ]] || return 1
|
|
177
|
+
case "$output" in
|
|
178
|
+
*"working directory = $APP_DIR"*|*"cd $APP_DIR "*) return 0 ;;
|
|
179
|
+
*) return 1 ;;
|
|
180
|
+
esac
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
remove_stale_pid() {
|
|
184
|
+
local pid
|
|
185
|
+
pid="$(read_pid || true)"
|
|
186
|
+
if [[ -n "$pid" && ! "$(is_running "$pid" && echo yes || echo no)" == yes ]]; then
|
|
187
|
+
rm -f "$PID_PATH"
|
|
188
|
+
fi
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
node_health_check() {
|
|
192
|
+
"$NODE_BIN" -e '
|
|
193
|
+
const http = require("node:http");
|
|
194
|
+
const url = process.argv[1];
|
|
195
|
+
const request = http.get(url, { timeout: 1000 }, (response) => {
|
|
196
|
+
response.resume();
|
|
197
|
+
process.exit(response.statusCode && response.statusCode >= 200 && response.statusCode < 500 ? 0 : 1);
|
|
198
|
+
});
|
|
199
|
+
request.on("timeout", () => request.destroy());
|
|
200
|
+
request.on("error", () => process.exit(1));
|
|
201
|
+
' "$(health_url)" >/dev/null 2>&1
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
start_daemon() {
|
|
205
|
+
cd "$APP_DIR"
|
|
206
|
+
if command -v setsid >/dev/null 2>&1; then
|
|
207
|
+
nohup setsid "$NODE_BIN" "$RUNTIME_ENTRY" >> "$LOG_PATH" 2>&1 &
|
|
208
|
+
else
|
|
209
|
+
nohup "$NODE_BIN" "$RUNTIME_ENTRY" >> "$LOG_PATH" 2>&1 &
|
|
210
|
+
fi
|
|
211
|
+
local pid="$!"
|
|
212
|
+
disown "$pid" 2>/dev/null || true
|
|
213
|
+
echo "$pid" > "$PID_PATH"
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
start_launchd() {
|
|
217
|
+
local domain
|
|
218
|
+
domain="$(launchd_domain)" || return 1
|
|
219
|
+
launchctl kickstart -k "$domain"
|
|
220
|
+
|
|
221
|
+
local pid
|
|
222
|
+
local deadline=$((SECONDS + HEALTH_TIMEOUT_SECONDS))
|
|
223
|
+
while (( SECONDS <= deadline )); do
|
|
224
|
+
pid="$(current_pid || true)"
|
|
225
|
+
if [[ -n "$pid" ]] && wait_for_health "$pid"; then
|
|
226
|
+
info "NTerminal started via launchd pid=$pid url=$(public_url) log=$LOG_PATH"
|
|
227
|
+
return 0
|
|
228
|
+
fi
|
|
229
|
+
sleep 0.25
|
|
230
|
+
done
|
|
231
|
+
|
|
232
|
+
echo "NTerminal failed to become healthy via launchd. Last log lines:" >&2
|
|
233
|
+
tail -n 40 "$LOG_PATH" >&2 || true
|
|
234
|
+
return 1
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
wait_for_health() {
|
|
238
|
+
local pid="$1"
|
|
239
|
+
local deadline=$((SECONDS + HEALTH_TIMEOUT_SECONDS))
|
|
240
|
+
while (( SECONDS <= deadline )); do
|
|
241
|
+
if ! is_running "$pid"; then
|
|
242
|
+
return 1
|
|
243
|
+
fi
|
|
244
|
+
if node_health_check; then
|
|
245
|
+
return 0
|
|
246
|
+
fi
|
|
247
|
+
sleep 0.25
|
|
248
|
+
done
|
|
249
|
+
return 1
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
port_owner_pid() {
|
|
253
|
+
command -v lsof >/dev/null 2>&1 || return 1
|
|
254
|
+
lsof -nP -iTCP:"$PORT" -sTCP:LISTEN -t 2>/dev/null | head -n 1
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
assert_port_available() {
|
|
258
|
+
local owner
|
|
259
|
+
owner="$(port_owner_pid || true)"
|
|
260
|
+
[[ -z "$owner" ]] && return 0
|
|
261
|
+
local pid
|
|
262
|
+
pid="$(current_pid || true)"
|
|
263
|
+
[[ -n "$pid" && "$owner" == "$pid" ]] && return 0
|
|
264
|
+
die "port $PORT is already in use by pid $owner"
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
cmd_status() {
|
|
268
|
+
remove_stale_pid
|
|
269
|
+
local pid
|
|
270
|
+
pid="$(current_pid || true)"
|
|
271
|
+
if [[ -n "$pid" ]]; then
|
|
272
|
+
info "NTerminal running pid=$pid url=$(public_url) log=$LOG_PATH"
|
|
273
|
+
return 0
|
|
274
|
+
fi
|
|
275
|
+
info "NTerminal stopped pid_file=$PID_PATH"
|
|
276
|
+
return 3
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
cmd_start() {
|
|
280
|
+
ensure_app_dir
|
|
281
|
+
ensure_env_file
|
|
282
|
+
ensure_runtime
|
|
283
|
+
validate_required_env || die "generate real secrets before starting"
|
|
284
|
+
ensure_directories
|
|
285
|
+
remove_stale_pid
|
|
286
|
+
|
|
287
|
+
local pid
|
|
288
|
+
pid="$(current_pid || true)"
|
|
289
|
+
if [[ -n "$pid" ]]; then
|
|
290
|
+
info "NTerminal already running pid=$pid url=$(public_url)"
|
|
291
|
+
return 0
|
|
292
|
+
fi
|
|
293
|
+
|
|
294
|
+
if launchd_controls_app; then
|
|
295
|
+
start_launchd
|
|
296
|
+
return 0
|
|
297
|
+
fi
|
|
298
|
+
|
|
299
|
+
assert_port_available
|
|
300
|
+
start_daemon
|
|
301
|
+
chmod 600 "$PID_PATH" 2>/dev/null || true
|
|
302
|
+
pid="$(read_pid)"
|
|
303
|
+
|
|
304
|
+
if wait_for_health "$pid"; then
|
|
305
|
+
info "NTerminal started pid=$pid url=$(public_url) log=$LOG_PATH"
|
|
306
|
+
return 0
|
|
307
|
+
fi
|
|
308
|
+
|
|
309
|
+
local exit_hint="failed to become healthy"
|
|
310
|
+
if ! is_running "$pid"; then
|
|
311
|
+
exit_hint="process exited during startup"
|
|
312
|
+
fi
|
|
313
|
+
rm -f "$PID_PATH"
|
|
314
|
+
echo "NTerminal $exit_hint. Last log lines:" >&2
|
|
315
|
+
tail -n 40 "$LOG_PATH" >&2 || true
|
|
316
|
+
return 1
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
cmd_stop() {
|
|
320
|
+
remove_stale_pid
|
|
321
|
+
local pid
|
|
322
|
+
pid="$(current_pid || true)"
|
|
323
|
+
if [[ -z "$pid" ]]; then
|
|
324
|
+
info "NTerminal already stopped"
|
|
325
|
+
return 0
|
|
326
|
+
fi
|
|
327
|
+
|
|
328
|
+
kill -TERM "$pid" 2>/dev/null || true
|
|
329
|
+
local deadline=$((SECONDS + STOP_TIMEOUT_SECONDS))
|
|
330
|
+
while (( SECONDS <= deadline )); do
|
|
331
|
+
if ! is_running "$pid"; then
|
|
332
|
+
rm -f "$PID_PATH"
|
|
333
|
+
info "NTerminal stopped pid=$pid"
|
|
334
|
+
return 0
|
|
335
|
+
fi
|
|
336
|
+
sleep 0.25
|
|
337
|
+
done
|
|
338
|
+
|
|
339
|
+
kill -KILL "$pid" 2>/dev/null || true
|
|
340
|
+
rm -f "$PID_PATH"
|
|
341
|
+
info "NTerminal force-stopped pid=$pid"
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
cmd_restart() {
|
|
345
|
+
if launchd_controls_app; then
|
|
346
|
+
start_launchd
|
|
347
|
+
else
|
|
348
|
+
cmd_stop >/dev/null
|
|
349
|
+
cmd_start
|
|
350
|
+
fi
|
|
351
|
+
info "NTerminal restarted"
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
cmd_build() {
|
|
355
|
+
ensure_app_dir
|
|
356
|
+
(
|
|
357
|
+
cd "$APP_DIR"
|
|
358
|
+
"$NPM_BIN" run build
|
|
359
|
+
)
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
cmd_deploy() {
|
|
363
|
+
ensure_app_dir
|
|
364
|
+
ensure_env_file
|
|
365
|
+
(
|
|
366
|
+
cd "$APP_DIR"
|
|
367
|
+
if [[ -f package-lock.json ]]; then
|
|
368
|
+
"$NPM_BIN" ci
|
|
369
|
+
else
|
|
370
|
+
"$NPM_BIN" install
|
|
371
|
+
fi
|
|
372
|
+
"$NPM_BIN" run typecheck
|
|
373
|
+
# node-pty's install script always compiles from source (no prebuilt
|
|
374
|
+
# download); ask npm to rebuild it explicitly after dependency updates.
|
|
375
|
+
"$NPM_BIN" rebuild node-pty --build-from-source
|
|
376
|
+
"$NPM_BIN" run build
|
|
377
|
+
)
|
|
378
|
+
cmd_restart
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
cmd_logs() {
|
|
382
|
+
local follow=0
|
|
383
|
+
local lines=100
|
|
384
|
+
if [[ "${1:-}" == "-f" || "${1:-}" == "follow" ]]; then
|
|
385
|
+
follow=1
|
|
386
|
+
shift || true
|
|
387
|
+
fi
|
|
388
|
+
if [[ "${1:-}" =~ ^[0-9]+$ ]]; then
|
|
389
|
+
lines="$1"
|
|
390
|
+
fi
|
|
391
|
+
[[ -f "$LOG_PATH" ]] || die "log file not found: $LOG_PATH"
|
|
392
|
+
if (( follow == 1 )); then
|
|
393
|
+
tail -n "$lines" -f "$LOG_PATH"
|
|
394
|
+
else
|
|
395
|
+
tail -n "$lines" "$LOG_PATH"
|
|
396
|
+
fi
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
stop_pid_file() {
|
|
400
|
+
local pid_path="$1"
|
|
401
|
+
local pid
|
|
402
|
+
pid="$(read_pid_file "$pid_path" || true)"
|
|
403
|
+
if [[ -z "$pid" ]]; then
|
|
404
|
+
return 0
|
|
405
|
+
fi
|
|
406
|
+
if ! is_running "$pid"; then
|
|
407
|
+
rm -f "$pid_path"
|
|
408
|
+
return 0
|
|
409
|
+
fi
|
|
410
|
+
|
|
411
|
+
kill -TERM "$pid" 2>/dev/null || true
|
|
412
|
+
local deadline=$((SECONDS + STOP_TIMEOUT_SECONDS))
|
|
413
|
+
while (( SECONDS <= deadline )); do
|
|
414
|
+
if ! is_running "$pid"; then
|
|
415
|
+
rm -f "$pid_path"
|
|
416
|
+
return 0
|
|
417
|
+
fi
|
|
418
|
+
sleep 0.25
|
|
419
|
+
done
|
|
420
|
+
|
|
421
|
+
kill -KILL "$pid" 2>/dev/null || true
|
|
422
|
+
rm -f "$pid_path"
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
clean_state_paths() {
|
|
426
|
+
local state_path="$1"
|
|
427
|
+
local pid_path="$2"
|
|
428
|
+
local log_path="$3"
|
|
429
|
+
local default_state_dir="$4"
|
|
430
|
+
local state_dir
|
|
431
|
+
state_dir="$(dirname "$state_path")"
|
|
432
|
+
if [[ "$state_dir" == "$default_state_dir" ]]; then
|
|
433
|
+
rm -rf "$state_dir"
|
|
434
|
+
else
|
|
435
|
+
rm -f "$state_path" "$pid_path" "$log_path" "$state_dir/update.lock"
|
|
436
|
+
rm -rf "$state_dir/notification-assets"
|
|
437
|
+
fi
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
cmd_clean() {
|
|
441
|
+
local force=0
|
|
442
|
+
local remove_env=0
|
|
443
|
+
local kill_tmux=1
|
|
444
|
+
while [[ $# -gt 0 ]]; do
|
|
445
|
+
case "$1" in
|
|
446
|
+
--force) force=1 ;;
|
|
447
|
+
--env) remove_env=1 ;;
|
|
448
|
+
--keep-tmux) kill_tmux=0 ;;
|
|
449
|
+
*) die "unknown clean option: $1" ;;
|
|
450
|
+
esac
|
|
451
|
+
shift
|
|
452
|
+
done
|
|
453
|
+
|
|
454
|
+
if (( force != 1 )); then
|
|
455
|
+
die "clean is destructive; rerun with: scripts/nterminalctl clean --force [--env] [--keep-tmux]"
|
|
456
|
+
fi
|
|
457
|
+
|
|
458
|
+
ensure_app_dir
|
|
459
|
+
cmd_stop >/dev/null
|
|
460
|
+
if (( kill_tmux == 1 )) && command -v tmux >/dev/null 2>&1; then
|
|
461
|
+
tmux -L nterminal kill-server >/dev/null 2>&1 || true
|
|
462
|
+
fi
|
|
463
|
+
|
|
464
|
+
local state_dir
|
|
465
|
+
state_dir="$(dirname "$STATE_PATH")"
|
|
466
|
+
clean_state_paths "$STATE_PATH" "$PID_PATH" "$LOG_PATH" "$APP_DIR/.nterminal"
|
|
467
|
+
if (( remove_env == 1 )); then
|
|
468
|
+
rm -f "$ENV_FILE"
|
|
469
|
+
fi
|
|
470
|
+
|
|
471
|
+
info "NTerminal cleaned state_dir=$state_dir env_removed=$remove_env tmux_killed=$kill_tmux"
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
cmd_pid() {
|
|
475
|
+
local pid
|
|
476
|
+
pid="$(current_pid || true)"
|
|
477
|
+
[[ -n "$pid" ]] || return 3
|
|
478
|
+
echo "$pid"
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
cmd_doctor() {
|
|
482
|
+
local failed=0
|
|
483
|
+
ensure_app_dir
|
|
484
|
+
|
|
485
|
+
if command -v "$NODE_BIN" >/dev/null 2>&1; then
|
|
486
|
+
local node_major
|
|
487
|
+
node_major="$("$NODE_BIN" -p 'Number(process.versions.node.split(".")[0])')"
|
|
488
|
+
if (( node_major < 22 )); then
|
|
489
|
+
echo "fail node: version must be >=22"
|
|
490
|
+
failed=1
|
|
491
|
+
else
|
|
492
|
+
echo "ok node: $("$NODE_BIN" --version)"
|
|
493
|
+
fi
|
|
494
|
+
else
|
|
495
|
+
echo "fail node: command not found: $NODE_BIN"
|
|
496
|
+
failed=1
|
|
497
|
+
fi
|
|
498
|
+
|
|
499
|
+
if command -v "$NPM_BIN" >/dev/null 2>&1; then
|
|
500
|
+
echo "ok npm: $("$NPM_BIN" --version)"
|
|
501
|
+
else
|
|
502
|
+
echo "fail npm: command not found: $NPM_BIN"
|
|
503
|
+
failed=1
|
|
504
|
+
fi
|
|
505
|
+
|
|
506
|
+
if [[ -f "$ENV_FILE" ]]; then
|
|
507
|
+
echo "ok env: $ENV_FILE"
|
|
508
|
+
validate_required_env || failed=1
|
|
509
|
+
else
|
|
510
|
+
echo "fail env: .env not found at $ENV_FILE"
|
|
511
|
+
failed=1
|
|
512
|
+
fi
|
|
513
|
+
|
|
514
|
+
if [[ -f "$RUNTIME_ENTRY" ]]; then
|
|
515
|
+
echo "ok runtime: dist/server/index.js"
|
|
516
|
+
else
|
|
517
|
+
echo "fail runtime: dist/server/index.js not found"
|
|
518
|
+
failed=1
|
|
519
|
+
fi
|
|
520
|
+
|
|
521
|
+
ensure_directories
|
|
522
|
+
if [[ -w "$(dirname "$PID_PATH")" && -w "$(dirname "$LOG_PATH")" ]]; then
|
|
523
|
+
echo "ok writable: pid/log directories"
|
|
524
|
+
else
|
|
525
|
+
echo "fail writable: pid/log directories"
|
|
526
|
+
failed=1
|
|
527
|
+
fi
|
|
528
|
+
|
|
529
|
+
local pid owner
|
|
530
|
+
pid="$(current_pid || true)"
|
|
531
|
+
owner="$(port_owner_pid || true)"
|
|
532
|
+
if [[ -z "$owner" || -n "$pid" && "$owner" == "$pid" ]]; then
|
|
533
|
+
echo "ok port: $PORT"
|
|
534
|
+
else
|
|
535
|
+
echo "fail port: $PORT owned by pid $owner"
|
|
536
|
+
failed=1
|
|
537
|
+
fi
|
|
538
|
+
|
|
539
|
+
return "$failed"
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
cmd_help() {
|
|
543
|
+
cat <<EOF
|
|
544
|
+
Usage: scripts/nterminalctl <command>
|
|
545
|
+
|
|
546
|
+
Commands:
|
|
547
|
+
status Show process status. Exit 0 when running, 3 when stopped.
|
|
548
|
+
start Start the built server in the background.
|
|
549
|
+
stop Stop the managed server with SIGTERM, then SIGKILL after timeout.
|
|
550
|
+
restart Stop and start the server.
|
|
551
|
+
build Run npm run build.
|
|
552
|
+
deploy npm ci/install, typecheck, rebuild node-pty, build, then restart.
|
|
553
|
+
clean --force Stop server, kill NTerminal tmux sessions, and remove local state/log/pid files.
|
|
554
|
+
Add --env to also remove .env. Add --keep-tmux to preserve live panes.
|
|
555
|
+
logs [N] Show the last N log lines. Default: 100.
|
|
556
|
+
logs -f [N] Follow logs.
|
|
557
|
+
doctor Check Node/npm/env/runtime/pid/log/port readiness.
|
|
558
|
+
pid Print the running PID. Exit 3 when stopped.
|
|
559
|
+
url Print the configured local URL.
|
|
560
|
+
help Show this help.
|
|
561
|
+
|
|
562
|
+
Environment:
|
|
563
|
+
NTERMINAL_APP_DIR Override app directory.
|
|
564
|
+
NTERMINAL_ENV_FILE Override .env path.
|
|
565
|
+
NTERMINAL_STATE_PATH Default .nterminal/state.json.
|
|
566
|
+
NTERMINAL_PID_PATH Default .nterminal/nterminal.pid.
|
|
567
|
+
NTERMINAL_LOG_PATH Default .nterminal/nterminal.log.
|
|
568
|
+
NTERMINAL_HEALTH_TIMEOUT_SECONDS Default 15.
|
|
569
|
+
NTERMINAL_STOP_TIMEOUT_SECONDS Default 20.
|
|
570
|
+
NTERMINAL_LAUNCHD_LABEL Default com.nterminal.local. Used when a loaded LaunchAgent controls this app.
|
|
571
|
+
EOF
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
case "$COMMAND" in
|
|
575
|
+
status) cmd_status "$@" ;;
|
|
576
|
+
start) cmd_start "$@" ;;
|
|
577
|
+
stop) cmd_stop "$@" ;;
|
|
578
|
+
restart|reload) cmd_restart "$@" ;;
|
|
579
|
+
build) cmd_build "$@" ;;
|
|
580
|
+
deploy) cmd_deploy "$@" ;;
|
|
581
|
+
clean|purge) cmd_clean "$@" ;;
|
|
582
|
+
logs|log) cmd_logs "$@" ;;
|
|
583
|
+
doctor) cmd_doctor "$@" ;;
|
|
584
|
+
pid) cmd_pid "$@" ;;
|
|
585
|
+
url) public_url ;;
|
|
586
|
+
help|-h|--help) cmd_help ;;
|
|
587
|
+
*) cmd_help >&2; die "unknown command: $COMMAND" ;;
|
|
588
|
+
esac
|