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.
Files changed (197) hide show
  1. package/.env.example +12 -0
  2. package/LICENSE +674 -0
  3. package/README.md +181 -0
  4. package/assets/brand/app-icon-1024.png +0 -0
  5. package/assets/brand/app-icon-384.png +0 -0
  6. package/assets/brand/apple-touch-icon-360.png +0 -0
  7. package/assets/brand/favicon-32.png +0 -0
  8. package/assets/brand/favicon-64.png +0 -0
  9. package/assets/brand/favicon-96.png +0 -0
  10. package/assets/brand/favicon.svg +4 -0
  11. package/assets/brand/nterminal-mark-64.png +0 -0
  12. package/assets/brand/nterminal-mark.svg +4 -0
  13. package/assets/brand/nterminal-wordmark-486x68.png +0 -0
  14. package/assets/brand/nterminal-wordmark.svg +3 -0
  15. package/assets/screenshot/scr.png +0 -0
  16. package/bin/nterminal.js +114 -0
  17. package/dist/client/apple-touch-icon.png +0 -0
  18. package/dist/client/assets/MarkdownPreview-BeDi-V7k.js +29 -0
  19. package/dist/client/assets/MesloLGS-NF-Bold-Italic-DwFsXcwX.ttf +0 -0
  20. package/dist/client/assets/MesloLGS-NF-Bold-kN-HYz-g.ttf +0 -0
  21. package/dist/client/assets/MesloLGS-NF-Italic-CMg1T6-G.ttf +0 -0
  22. package/dist/client/assets/MesloLGS-NF-Regular-Cxr8pvCI.ttf +0 -0
  23. package/dist/client/assets/index-BQkKYjXb.js +33 -0
  24. package/dist/client/assets/index-WqeS39wU.css +1 -0
  25. package/dist/client/assets/notifications/character-2258.mp4 +0 -0
  26. package/dist/client/assets/notifications/character-2260.mp4 +0 -0
  27. package/dist/client/assets/notifications/character-2272.mp4 +0 -0
  28. package/dist/client/brand/nterminal-mark-64.png +0 -0
  29. package/dist/client/brand/nterminal-mark.svg +4 -0
  30. package/dist/client/brand/nterminal-wordmark-486x68.png +0 -0
  31. package/dist/client/brand/nterminal-wordmark.svg +3 -0
  32. package/dist/client/icons/app-icon-1024.png +0 -0
  33. package/dist/client/icons/app-icon-384.png +0 -0
  34. package/dist/client/icons/favicon-32.png +0 -0
  35. package/dist/client/icons/favicon-64.png +0 -0
  36. package/dist/client/icons/favicon-96.png +0 -0
  37. package/dist/client/icons/favicon.svg +4 -0
  38. package/dist/client/index.html +21 -0
  39. package/dist/client/manifest.webmanifest +24 -0
  40. package/dist/scripts/generate-secrets.js +3 -0
  41. package/dist/scripts/generate-secrets.js.map +1 -0
  42. package/dist/scripts/onboarding.js +814 -0
  43. package/dist/scripts/onboarding.js.map +1 -0
  44. package/dist/scripts/proxySetup.js +1007 -0
  45. package/dist/scripts/proxySetup.js.map +1 -0
  46. package/dist/server/agent/agentAuth.d.ts +6 -0
  47. package/dist/server/agent/agentAuth.js +35 -0
  48. package/dist/server/agent/agentAuth.js.map +1 -0
  49. package/dist/server/agent/agentProxy.d.ts +5 -0
  50. package/dist/server/agent/agentProxy.js +63 -0
  51. package/dist/server/agent/agentProxy.js.map +1 -0
  52. package/dist/server/agent/agentRoutes.d.ts +9 -0
  53. package/dist/server/agent/agentRoutes.js +327 -0
  54. package/dist/server/agent/agentRoutes.js.map +1 -0
  55. package/dist/server/agent/agentWebSocketProxy.d.ts +3 -0
  56. package/dist/server/agent/agentWebSocketProxy.js +65 -0
  57. package/dist/server/agent/agentWebSocketProxy.js.map +1 -0
  58. package/dist/server/auth/authService.d.ts +100 -0
  59. package/dist/server/auth/authService.js +415 -0
  60. package/dist/server/auth/authService.js.map +1 -0
  61. package/dist/server/auth/cookies.d.ts +11 -0
  62. package/dist/server/auth/cookies.js +39 -0
  63. package/dist/server/auth/cookies.js.map +1 -0
  64. package/dist/server/auth/ipMatch.d.ts +14 -0
  65. package/dist/server/auth/ipMatch.js +103 -0
  66. package/dist/server/auth/ipMatch.js.map +1 -0
  67. package/dist/server/auth/rateLimit.d.ts +17 -0
  68. package/dist/server/auth/rateLimit.js +25 -0
  69. package/dist/server/auth/rateLimit.js.map +1 -0
  70. package/dist/server/auth/totpService.d.ts +10 -0
  71. package/dist/server/auth/totpService.js +37 -0
  72. package/dist/server/auth/totpService.js.map +1 -0
  73. package/dist/server/config.d.ts +27 -0
  74. package/dist/server/config.js +138 -0
  75. package/dist/server/config.js.map +1 -0
  76. package/dist/server/files/fileExplorerService.d.ts +38 -0
  77. package/dist/server/files/fileExplorerService.js +551 -0
  78. package/dist/server/files/fileExplorerService.js.map +1 -0
  79. package/dist/server/files/rootToken.d.ts +51 -0
  80. package/dist/server/files/rootToken.js +139 -0
  81. package/dist/server/files/rootToken.js.map +1 -0
  82. package/dist/server/http.d.ts +13 -0
  83. package/dist/server/http.js +69 -0
  84. package/dist/server/http.js.map +1 -0
  85. package/dist/server/index.d.ts +1 -0
  86. package/dist/server/index.js +45 -0
  87. package/dist/server/index.js.map +1 -0
  88. package/dist/server/routes/agentManagementRoutes.d.ts +9 -0
  89. package/dist/server/routes/agentManagementRoutes.js +304 -0
  90. package/dist/server/routes/agentManagementRoutes.js.map +1 -0
  91. package/dist/server/routes/authRoutes.d.ts +10 -0
  92. package/dist/server/routes/authRoutes.js +95 -0
  93. package/dist/server/routes/authRoutes.js.map +1 -0
  94. package/dist/server/routes/fileRoutes.d.ts +11 -0
  95. package/dist/server/routes/fileRoutes.js +185 -0
  96. package/dist/server/routes/fileRoutes.js.map +1 -0
  97. package/dist/server/routes/notificationAssetRoutes.d.ts +9 -0
  98. package/dist/server/routes/notificationAssetRoutes.js +280 -0
  99. package/dist/server/routes/notificationAssetRoutes.js.map +1 -0
  100. package/dist/server/routes/securityRoutes.d.ts +7 -0
  101. package/dist/server/routes/securityRoutes.js +53 -0
  102. package/dist/server/routes/securityRoutes.js.map +1 -0
  103. package/dist/server/routes/socketBackpressure.d.ts +26 -0
  104. package/dist/server/routes/socketBackpressure.js +63 -0
  105. package/dist/server/routes/socketBackpressure.js.map +1 -0
  106. package/dist/server/routes/terminalLayoutRoutes.d.ts +9 -0
  107. package/dist/server/routes/terminalLayoutRoutes.js +108 -0
  108. package/dist/server/routes/terminalLayoutRoutes.js.map +1 -0
  109. package/dist/server/routes/terminalRoutes.d.ts +14 -0
  110. package/dist/server/routes/terminalRoutes.js +177 -0
  111. package/dist/server/routes/terminalRoutes.js.map +1 -0
  112. package/dist/server/routes/terminalWebSocket.d.ts +9 -0
  113. package/dist/server/routes/terminalWebSocket.js +129 -0
  114. package/dist/server/routes/terminalWebSocket.js.map +1 -0
  115. package/dist/server/routes/totpRoutes.d.ts +7 -0
  116. package/dist/server/routes/totpRoutes.js +46 -0
  117. package/dist/server/routes/totpRoutes.js.map +1 -0
  118. package/dist/server/routes/updateRoutes.d.ts +7 -0
  119. package/dist/server/routes/updateRoutes.js +24 -0
  120. package/dist/server/routes/updateRoutes.js.map +1 -0
  121. package/dist/server/routes/uploadRoutes.d.ts +9 -0
  122. package/dist/server/routes/uploadRoutes.js +95 -0
  123. package/dist/server/routes/uploadRoutes.js.map +1 -0
  124. package/dist/server/storage/fileStore.d.ts +90 -0
  125. package/dist/server/storage/fileStore.js +275 -0
  126. package/dist/server/storage/fileStore.js.map +1 -0
  127. package/dist/server/system/stats.d.ts +2 -0
  128. package/dist/server/system/stats.js +37 -0
  129. package/dist/server/system/stats.js.map +1 -0
  130. package/dist/server/terminal/NodePtyAdapter.d.ts +4 -0
  131. package/dist/server/terminal/NodePtyAdapter.js +14 -0
  132. package/dist/server/terminal/NodePtyAdapter.js.map +1 -0
  133. package/dist/server/terminal/PtyAdapter.d.ts +57 -0
  134. package/dist/server/terminal/PtyAdapter.js +2 -0
  135. package/dist/server/terminal/PtyAdapter.js.map +1 -0
  136. package/dist/server/terminal/TerminalManager.d.ts +74 -0
  137. package/dist/server/terminal/TerminalManager.js +561 -0
  138. package/dist/server/terminal/TerminalManager.js.map +1 -0
  139. package/dist/server/terminal/TmuxPtyAdapter.d.ts +25 -0
  140. package/dist/server/terminal/TmuxPtyAdapter.js +543 -0
  141. package/dist/server/terminal/TmuxPtyAdapter.js.map +1 -0
  142. package/dist/server/terminal/codexTranscriptSource.d.ts +9 -0
  143. package/dist/server/terminal/codexTranscriptSource.js +144 -0
  144. package/dist/server/terminal/codexTranscriptSource.js.map +1 -0
  145. package/dist/server/terminal/cwdResolver.d.ts +8 -0
  146. package/dist/server/terminal/cwdResolver.js +37 -0
  147. package/dist/server/terminal/cwdResolver.js.map +1 -0
  148. package/dist/server/terminal/outputBuffer.d.ts +7 -0
  149. package/dist/server/terminal/outputBuffer.js +17 -0
  150. package/dist/server/terminal/outputBuffer.js.map +1 -0
  151. package/dist/server/terminal/transcriptHistory.d.ts +7 -0
  152. package/dist/server/terminal/transcriptHistory.js +315 -0
  153. package/dist/server/terminal/transcriptHistory.js.map +1 -0
  154. package/dist/server/update/gitUpdate.d.ts +27 -0
  155. package/dist/server/update/gitUpdate.js +241 -0
  156. package/dist/server/update/gitUpdate.js.map +1 -0
  157. package/dist/server/uploads/uploadPaths.d.ts +18 -0
  158. package/dist/server/uploads/uploadPaths.js +116 -0
  159. package/dist/server/uploads/uploadPaths.js.map +1 -0
  160. package/dist/server/uploads/uploadService.d.ts +21 -0
  161. package/dist/server/uploads/uploadService.js +230 -0
  162. package/dist/server/uploads/uploadService.js.map +1 -0
  163. package/dist/shared/layoutState.d.ts +6 -0
  164. package/dist/shared/layoutState.js +115 -0
  165. package/dist/shared/layoutState.js.map +1 -0
  166. package/dist/shared/notificationAssets.d.ts +9 -0
  167. package/dist/shared/notificationAssets.js +27 -0
  168. package/dist/shared/notificationAssets.js.map +1 -0
  169. package/dist/shared/protocol.d.ts +308 -0
  170. package/dist/shared/protocol.js +29 -0
  171. package/dist/shared/protocol.js.map +1 -0
  172. package/dist/shared/types.d.ts +56 -0
  173. package/dist/shared/types.js +2 -0
  174. package/dist/shared/types.js.map +1 -0
  175. package/docs/assets/nterminal-workspace.png +0 -0
  176. package/docs/configuration.md +97 -0
  177. package/docs/features.md +126 -0
  178. package/docs/onboarding.md +122 -0
  179. package/docs/operations.md +112 -0
  180. package/docs/terminal-history.md +54 -0
  181. package/package.json +85 -0
  182. package/public/apple-touch-icon.png +0 -0
  183. package/public/assets/notifications/character-2258.mp4 +0 -0
  184. package/public/assets/notifications/character-2260.mp4 +0 -0
  185. package/public/assets/notifications/character-2272.mp4 +0 -0
  186. package/public/brand/nterminal-mark-64.png +0 -0
  187. package/public/brand/nterminal-mark.svg +4 -0
  188. package/public/brand/nterminal-wordmark-486x68.png +0 -0
  189. package/public/brand/nterminal-wordmark.svg +3 -0
  190. package/public/icons/app-icon-1024.png +0 -0
  191. package/public/icons/app-icon-384.png +0 -0
  192. package/public/icons/favicon-32.png +0 -0
  193. package/public/icons/favicon-64.png +0 -0
  194. package/public/icons/favicon-96.png +0 -0
  195. package/public/icons/favicon.svg +4 -0
  196. package/public/manifest.webmanifest +24 -0
  197. 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