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,241 @@
|
|
|
1
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
const updateLockStaleMs = 10 * 60 * 1000;
|
|
5
|
+
const packageUpdateTimeoutMs = 5 * 60 * 1000;
|
|
6
|
+
function appDir() {
|
|
7
|
+
return process.env.NTERMINAL_APP_DIR ? path.resolve(process.env.NTERMINAL_APP_DIR) : process.cwd();
|
|
8
|
+
}
|
|
9
|
+
function git(args, cwd) {
|
|
10
|
+
const result = spawnSync('git', args, { cwd, encoding: 'utf8', timeout: 60_000 });
|
|
11
|
+
return {
|
|
12
|
+
ok: result.status === 0,
|
|
13
|
+
stdout: (result.stdout ?? '').trim(),
|
|
14
|
+
stderr: (result.stderr ?? '').trim()
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
function npm(args, cwd, timeout = 60_000) {
|
|
18
|
+
const result = spawnSync(process.env.NTERMINAL_NPM_BIN || 'npm', args, { cwd, encoding: 'utf8', timeout });
|
|
19
|
+
return {
|
|
20
|
+
ok: result.status === 0,
|
|
21
|
+
stdout: (result.stdout ?? '').trim(),
|
|
22
|
+
stderr: (result.stderr ?? '').trim()
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function isGitRepo(cwd) {
|
|
26
|
+
// Authoritative check — an empty/partial `.git` dir alone shouldn't count as a repo.
|
|
27
|
+
const result = git(['rev-parse', '--is-inside-work-tree'], cwd);
|
|
28
|
+
return result.ok && result.stdout === 'true';
|
|
29
|
+
}
|
|
30
|
+
function readPackageName(cwd) {
|
|
31
|
+
try {
|
|
32
|
+
const pkg = JSON.parse(readFileSync(path.join(cwd, 'package.json'), 'utf8'));
|
|
33
|
+
return typeof pkg.name === 'string' && pkg.name.trim() ? pkg.name.trim() : 'nterminal';
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return 'nterminal';
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function readPackageVersion(cwd) {
|
|
40
|
+
try {
|
|
41
|
+
const pkg = JSON.parse(readFileSync(path.join(cwd, 'package.json'), 'utf8'));
|
|
42
|
+
return typeof pkg.version === 'string' ? pkg.version : 'unknown';
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return 'unknown';
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function readCommit(cwd) {
|
|
49
|
+
if (!isGitRepo(cwd)) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
const commit = git(['rev-parse', 'HEAD'], cwd);
|
|
53
|
+
return commit.ok ? commit.stdout : null;
|
|
54
|
+
}
|
|
55
|
+
// Frozen at process start. It only changes once a deploy has actually rebuilt and
|
|
56
|
+
// restarted the server — so the UI can tell a real restart apart from a bare `git pull`.
|
|
57
|
+
const startupVersion = readPackageVersion(appDir());
|
|
58
|
+
const startupCommit = readCommit(appDir());
|
|
59
|
+
// Cross-process guard (lockfile) so repeated clicks, multiple tabs, or a still-running deploy
|
|
60
|
+
// don't spawn overlapping deploys. Auto-expires so a crashed/hung deploy doesn't wedge updates.
|
|
61
|
+
function resolveRuntimePath(cwd, value) {
|
|
62
|
+
return path.isAbsolute(value) ? value : path.resolve(cwd, value);
|
|
63
|
+
}
|
|
64
|
+
function runtimeStateDir(cwd) {
|
|
65
|
+
const configured = process.env.NTERMINAL_STATE_PATH?.trim();
|
|
66
|
+
return configured ? path.dirname(resolveRuntimePath(cwd, configured)) : path.join(cwd, '.nterminal');
|
|
67
|
+
}
|
|
68
|
+
function lockPath(cwd) {
|
|
69
|
+
return path.join(runtimeStateDir(cwd), 'update.lock');
|
|
70
|
+
}
|
|
71
|
+
function updateLocked(cwd) {
|
|
72
|
+
const file = lockPath(cwd);
|
|
73
|
+
if (!existsSync(file)) {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
const stamped = Number(readFileSync(file, 'utf8').trim());
|
|
78
|
+
return Number.isFinite(stamped) && Date.now() - stamped < updateLockStaleMs;
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function acquireUpdateLock(cwd) {
|
|
85
|
+
mkdirSync(runtimeStateDir(cwd), { recursive: true });
|
|
86
|
+
writeFileSync(lockPath(cwd), String(Date.now()));
|
|
87
|
+
}
|
|
88
|
+
function releaseUpdateLock(cwd) {
|
|
89
|
+
rmSync(lockPath(cwd), { force: true });
|
|
90
|
+
}
|
|
91
|
+
export function getVersionInfo(cwd = appDir()) {
|
|
92
|
+
const version = readPackageVersion(cwd);
|
|
93
|
+
if (!isGitRepo(cwd)) {
|
|
94
|
+
return {
|
|
95
|
+
version,
|
|
96
|
+
runningVersion: startupVersion,
|
|
97
|
+
commit: null,
|
|
98
|
+
runningCommit: startupCommit,
|
|
99
|
+
branch: null,
|
|
100
|
+
dirty: false,
|
|
101
|
+
isGit: false,
|
|
102
|
+
installMode: 'package'
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
const commit = git(['rev-parse', 'HEAD'], cwd);
|
|
106
|
+
const branch = git(['rev-parse', '--abbrev-ref', 'HEAD'], cwd);
|
|
107
|
+
const status = git(['status', '--porcelain'], cwd);
|
|
108
|
+
return {
|
|
109
|
+
version,
|
|
110
|
+
runningVersion: startupVersion,
|
|
111
|
+
commit: commit.ok ? commit.stdout : null,
|
|
112
|
+
runningCommit: startupCommit,
|
|
113
|
+
branch: branch.ok ? branch.stdout : null,
|
|
114
|
+
dirty: status.ok ? status.stdout.length > 0 : false,
|
|
115
|
+
isGit: true,
|
|
116
|
+
installMode: 'git'
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
export function checkBehind(cwd = appDir()) {
|
|
120
|
+
if (!isGitRepo(cwd)) {
|
|
121
|
+
return checkPackageUpdate(cwd);
|
|
122
|
+
}
|
|
123
|
+
return checkGitBehind(cwd);
|
|
124
|
+
}
|
|
125
|
+
function checkGitBehind(cwd) {
|
|
126
|
+
const commit = git(['rev-parse', 'HEAD'], cwd);
|
|
127
|
+
const upstream = git(['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}'], cwd);
|
|
128
|
+
if (!upstream.ok) {
|
|
129
|
+
return { behind: null, commit: commit.ok ? commit.stdout : null, error: 'no_upstream' };
|
|
130
|
+
}
|
|
131
|
+
const fetched = git(['fetch'], cwd);
|
|
132
|
+
if (!fetched.ok) {
|
|
133
|
+
return { behind: null, commit: commit.ok ? commit.stdout : null, error: fetched.stderr || 'fetch_failed' };
|
|
134
|
+
}
|
|
135
|
+
const count = git(['rev-list', '--count', 'HEAD..@{u}'], cwd);
|
|
136
|
+
if (!count.ok) {
|
|
137
|
+
return { behind: null, commit: commit.ok ? commit.stdout : null, error: count.stderr || 'rev_list_failed' };
|
|
138
|
+
}
|
|
139
|
+
return { behind: Number(count.stdout) || 0, commit: commit.ok ? commit.stdout : null };
|
|
140
|
+
}
|
|
141
|
+
function checkPackageUpdate(cwd) {
|
|
142
|
+
const currentVersion = readPackageVersion(cwd);
|
|
143
|
+
const packageName = readPackageName(cwd);
|
|
144
|
+
const latest = npm(['view', `${packageName}@latest`, 'version'], cwd);
|
|
145
|
+
if (!latest.ok) {
|
|
146
|
+
return {
|
|
147
|
+
behind: null,
|
|
148
|
+
commit: null,
|
|
149
|
+
currentVersion,
|
|
150
|
+
latestVersion: null,
|
|
151
|
+
updateAvailable: null,
|
|
152
|
+
error: latest.stderr || latest.stdout || 'npm_view_failed'
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
const latestVersion = latest.stdout.split(/\r?\n/).at(-1)?.trim() || null;
|
|
156
|
+
return {
|
|
157
|
+
behind: null,
|
|
158
|
+
commit: null,
|
|
159
|
+
currentVersion,
|
|
160
|
+
latestVersion,
|
|
161
|
+
updateAvailable: latestVersion ? latestVersion !== currentVersion : null
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
export function runUpdate(cwd = appDir()) {
|
|
165
|
+
if (!isGitRepo(cwd)) {
|
|
166
|
+
return runPackageUpdate(cwd);
|
|
167
|
+
}
|
|
168
|
+
return runGitUpdate(cwd);
|
|
169
|
+
}
|
|
170
|
+
function runGitUpdate(cwd) {
|
|
171
|
+
if (updateLocked(cwd)) {
|
|
172
|
+
return { ok: false, changed: false, message: 'An update is already in progress on this server.', restarting: false };
|
|
173
|
+
}
|
|
174
|
+
// Refuse on any local modifications — ff-only alone only blocks *conflicting* changes,
|
|
175
|
+
// which would otherwise leave a dirty tree mixed with newly pulled code.
|
|
176
|
+
const status = git(['status', '--porcelain'], cwd);
|
|
177
|
+
if (status.ok && status.stdout.length > 0) {
|
|
178
|
+
return { ok: false, changed: false, message: 'Working tree has local changes — commit or discard them before updating.', restarting: false };
|
|
179
|
+
}
|
|
180
|
+
const pull = git(['pull', '--ff-only'], cwd);
|
|
181
|
+
if (!pull.ok) {
|
|
182
|
+
return {
|
|
183
|
+
ok: false,
|
|
184
|
+
changed: false,
|
|
185
|
+
message: pull.stderr || pull.stdout || 'git pull --ff-only failed',
|
|
186
|
+
restarting: false
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
const changed = !/Already up to date/i.test(pull.stdout);
|
|
190
|
+
if (!changed) {
|
|
191
|
+
return { ok: true, changed: false, message: 'Already up to date.', restarting: false };
|
|
192
|
+
}
|
|
193
|
+
// Build + restart in a detached process so it survives this server restarting itself.
|
|
194
|
+
acquireUpdateLock(cwd);
|
|
195
|
+
const deploy = spawn('bash', [path.join(cwd, 'scripts/nterminalctl'), 'deploy'], {
|
|
196
|
+
cwd,
|
|
197
|
+
detached: true,
|
|
198
|
+
env: { ...process.env, NTERMINAL_UPDATE_LOCK_PATH: lockPath(cwd) },
|
|
199
|
+
stdio: 'ignore'
|
|
200
|
+
});
|
|
201
|
+
deploy.unref();
|
|
202
|
+
return { ok: true, changed: true, message: pull.stdout || 'Updated; rebuilding and restarting.', restarting: true };
|
|
203
|
+
}
|
|
204
|
+
function runPackageUpdate(cwd) {
|
|
205
|
+
if (updateLocked(cwd)) {
|
|
206
|
+
return { ok: false, changed: false, message: 'An update is already in progress on this server.', restarting: false };
|
|
207
|
+
}
|
|
208
|
+
const check = checkPackageUpdate(cwd);
|
|
209
|
+
if (check.error) {
|
|
210
|
+
return { ok: false, changed: false, message: check.error, restarting: false };
|
|
211
|
+
}
|
|
212
|
+
if (!check.updateAvailable) {
|
|
213
|
+
return { ok: true, changed: false, message: 'Already up to date.', restarting: false };
|
|
214
|
+
}
|
|
215
|
+
acquireUpdateLock(cwd);
|
|
216
|
+
const packageName = readPackageName(cwd);
|
|
217
|
+
const install = npm(['install', '-g', `${packageName}@latest`], cwd, packageUpdateTimeoutMs);
|
|
218
|
+
if (!install.ok) {
|
|
219
|
+
releaseUpdateLock(cwd);
|
|
220
|
+
return {
|
|
221
|
+
ok: false,
|
|
222
|
+
changed: false,
|
|
223
|
+
message: install.stderr || install.stdout || 'npm install -g failed',
|
|
224
|
+
restarting: false
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
const restart = spawn('sh', ['-c', 'sleep 0.2; exec bash "$1" restart', 'nterminal-restart', path.join(cwd, 'scripts/nterminalctl')], {
|
|
228
|
+
cwd,
|
|
229
|
+
detached: true,
|
|
230
|
+
env: { ...process.env, NTERMINAL_UPDATE_LOCK_PATH: lockPath(cwd) },
|
|
231
|
+
stdio: 'ignore'
|
|
232
|
+
});
|
|
233
|
+
restart.unref();
|
|
234
|
+
return {
|
|
235
|
+
ok: true,
|
|
236
|
+
changed: true,
|
|
237
|
+
message: `Updated ${packageName} ${check.currentVersion ?? ''} -> ${check.latestVersion ?? 'latest'}; restarting.`,
|
|
238
|
+
restarting: true
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
//# sourceMappingURL=gitUpdate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gitUpdate.js","sourceRoot":"","sources":["../../../src/server/update/gitUpdate.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { UploadManifestEntry } from '../../shared/protocol.js';
|
|
2
|
+
export interface UploadPathFailure {
|
|
3
|
+
fieldName: string;
|
|
4
|
+
relativePath: string;
|
|
5
|
+
error: 'unsafe_relative_path';
|
|
6
|
+
}
|
|
7
|
+
export interface UploadTarget {
|
|
8
|
+
fieldName: string;
|
|
9
|
+
relativePath: string;
|
|
10
|
+
savedRelativePath: string;
|
|
11
|
+
absolutePath: string;
|
|
12
|
+
size: number;
|
|
13
|
+
}
|
|
14
|
+
export interface UploadTargetPlan {
|
|
15
|
+
targets: UploadTarget[];
|
|
16
|
+
failures: UploadPathFailure[];
|
|
17
|
+
}
|
|
18
|
+
export declare function planUploadTargets(destinationCwd: string, entries: UploadManifestEntry[]): Promise<UploadTargetPlan>;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { access } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
export async function planUploadTargets(destinationCwd, entries) {
|
|
4
|
+
const root = path.resolve(destinationCwd);
|
|
5
|
+
const reserved = new Set();
|
|
6
|
+
const topLevelFolders = new Map();
|
|
7
|
+
const targets = [];
|
|
8
|
+
const failures = [];
|
|
9
|
+
for (const entry of entries) {
|
|
10
|
+
const safe = safeEntry(entry);
|
|
11
|
+
if (!safe) {
|
|
12
|
+
failures.push({ fieldName: entry.fieldName, relativePath: entry.relativePath, error: 'unsafe_relative_path' });
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
if (safe.segments.length === 1) {
|
|
16
|
+
const savedRelativePath = await allocateFileRelativePath(root, [], safe.segments[0], reserved);
|
|
17
|
+
targets.push(targetFor(root, safe, savedRelativePath));
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
const originalTopLevel = safe.segments[0];
|
|
21
|
+
let savedTopLevel = topLevelFolders.get(originalTopLevel);
|
|
22
|
+
if (!savedTopLevel) {
|
|
23
|
+
savedTopLevel = await allocateFolderName(root, originalTopLevel, reserved);
|
|
24
|
+
topLevelFolders.set(originalTopLevel, savedTopLevel);
|
|
25
|
+
}
|
|
26
|
+
const parentSegments = [savedTopLevel, ...safe.segments.slice(1, -1)];
|
|
27
|
+
const fileName = safe.segments.at(-1);
|
|
28
|
+
const savedRelativePath = await allocateFileRelativePath(root, parentSegments, fileName, reserved);
|
|
29
|
+
targets.push(targetFor(root, safe, savedRelativePath));
|
|
30
|
+
}
|
|
31
|
+
return { targets, failures };
|
|
32
|
+
}
|
|
33
|
+
function safeEntry(entry) {
|
|
34
|
+
const segments = safeRelativeSegments(entry.relativePath);
|
|
35
|
+
if (!segments || !isFiniteNonNegativeInteger(entry.size) || !entry.fieldName.trim()) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
return { entry, segments };
|
|
39
|
+
}
|
|
40
|
+
function safeRelativeSegments(relativePath) {
|
|
41
|
+
if (typeof relativePath !== 'string' || relativePath.length === 0 || relativePath.includes('\0') || relativePath.includes('\\')) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
if (path.posix.isAbsolute(relativePath) || /^[A-Za-z]:/.test(relativePath)) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
const segments = relativePath.split('/');
|
|
48
|
+
if (segments.some((segment) => segment === '' || segment === '.' || segment === '..')) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
return segments;
|
|
52
|
+
}
|
|
53
|
+
async function allocateFolderName(root, name, reserved) {
|
|
54
|
+
let index = 1;
|
|
55
|
+
while (true) {
|
|
56
|
+
const candidate = index === 1 ? name : `${name} ${index}`;
|
|
57
|
+
const absolutePath = resolveContained(root, [candidate]);
|
|
58
|
+
if (!reserved.has(absolutePath) && !(await exists(absolutePath))) {
|
|
59
|
+
reserved.add(absolutePath);
|
|
60
|
+
return candidate;
|
|
61
|
+
}
|
|
62
|
+
index += 1;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
async function allocateFileRelativePath(root, parentSegments, fileName, reserved) {
|
|
66
|
+
let index = 1;
|
|
67
|
+
while (true) {
|
|
68
|
+
const candidateName = index === 1 ? fileName : suffixedFileName(fileName, index);
|
|
69
|
+
const candidateSegments = [...parentSegments, candidateName];
|
|
70
|
+
const absolutePath = resolveContained(root, candidateSegments);
|
|
71
|
+
if (!reserved.has(absolutePath) && !(await exists(absolutePath))) {
|
|
72
|
+
reserved.add(absolutePath);
|
|
73
|
+
return candidateSegments.join('/');
|
|
74
|
+
}
|
|
75
|
+
index += 1;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function targetFor(root, safe, savedRelativePath) {
|
|
79
|
+
const absolutePath = resolveContained(root, savedRelativePath.split('/'));
|
|
80
|
+
return {
|
|
81
|
+
fieldName: safe.entry.fieldName,
|
|
82
|
+
relativePath: safe.entry.relativePath,
|
|
83
|
+
savedRelativePath,
|
|
84
|
+
absolutePath,
|
|
85
|
+
size: safe.entry.size
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function resolveContained(root, segments) {
|
|
89
|
+
const resolved = path.resolve(root, ...segments);
|
|
90
|
+
const relative = path.relative(root, resolved);
|
|
91
|
+
if (relative === '' || relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
92
|
+
throw new Error('unsafe_relative_path');
|
|
93
|
+
}
|
|
94
|
+
return resolved;
|
|
95
|
+
}
|
|
96
|
+
function suffixedFileName(fileName, index) {
|
|
97
|
+
if (fileName.startsWith('.') && fileName.indexOf('.', 1) === -1) {
|
|
98
|
+
return `${fileName} ${index}`;
|
|
99
|
+
}
|
|
100
|
+
const extension = path.extname(fileName);
|
|
101
|
+
const base = extension ? fileName.slice(0, -extension.length) : fileName;
|
|
102
|
+
return `${base} ${index}${extension}`;
|
|
103
|
+
}
|
|
104
|
+
async function exists(filePath) {
|
|
105
|
+
try {
|
|
106
|
+
await access(filePath);
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
function isFiniteNonNegativeInteger(value) {
|
|
114
|
+
return Number.isSafeInteger(value) && value >= 0;
|
|
115
|
+
}
|
|
116
|
+
//# sourceMappingURL=uploadPaths.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"uploadPaths.js","sourceRoot":"","sources":["../../../src/server/uploads/uploadPaths.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type Readable } from 'node:stream';
|
|
2
|
+
import type { AppConfig } from '../config.js';
|
|
3
|
+
import type { UploadManifest, UploadResponse } from '../../shared/protocol.js';
|
|
4
|
+
export type UploadLimits = Pick<AppConfig, 'uploadMaxFiles' | 'uploadMaxFileBytes' | 'uploadMaxBatchBytes'>;
|
|
5
|
+
export interface CreateUploadSessionOptions {
|
|
6
|
+
destinationCwd: string;
|
|
7
|
+
limits: UploadLimits;
|
|
8
|
+
manifest: UploadManifest;
|
|
9
|
+
}
|
|
10
|
+
export declare class UploadRequestError extends Error {
|
|
11
|
+
readonly statusCode: number;
|
|
12
|
+
readonly code: string;
|
|
13
|
+
constructor(statusCode: number, code: string);
|
|
14
|
+
}
|
|
15
|
+
export interface UploadSession {
|
|
16
|
+
writeFile(fieldName: string, stream: Readable): Promise<void>;
|
|
17
|
+
finish(): Promise<UploadResponse>;
|
|
18
|
+
abort(): Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
export declare function createUploadSession(options: CreateUploadSessionOptions): Promise<UploadSession>;
|
|
21
|
+
export declare function parseUploadManifest(value: unknown): UploadManifest | null;
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { mkdir, open, realpath, unlink } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { Transform } from 'node:stream';
|
|
4
|
+
import { pipeline } from 'node:stream/promises';
|
|
5
|
+
import { planUploadTargets } from './uploadPaths.js';
|
|
6
|
+
export class UploadRequestError extends Error {
|
|
7
|
+
statusCode;
|
|
8
|
+
code;
|
|
9
|
+
constructor(statusCode, code) {
|
|
10
|
+
super(code);
|
|
11
|
+
this.statusCode = statusCode;
|
|
12
|
+
this.code = code;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export async function createUploadSession(options) {
|
|
16
|
+
validateManifest(options.manifest, options.limits);
|
|
17
|
+
const pathPlan = await planUploadTargets(options.destinationCwd, options.manifest.entries);
|
|
18
|
+
return new StreamingUploadSession(options.destinationCwd, options.manifest.entries, options.limits, pathPlan.targets, pathPlan.failures);
|
|
19
|
+
}
|
|
20
|
+
export function parseUploadManifest(value) {
|
|
21
|
+
if (!value || typeof value !== 'object') {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
const manifest = value;
|
|
25
|
+
if (typeof manifest.terminalId !== 'string' || !manifest.terminalId.trim() || !Array.isArray(manifest.entries)) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
const entries = [];
|
|
29
|
+
for (const entry of manifest.entries) {
|
|
30
|
+
if (!entry || typeof entry !== 'object') {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
const candidate = entry;
|
|
34
|
+
const fieldName = candidate.fieldName;
|
|
35
|
+
const relativePath = candidate.relativePath;
|
|
36
|
+
const size = candidate.size;
|
|
37
|
+
if (typeof fieldName !== 'string' ||
|
|
38
|
+
typeof relativePath !== 'string' ||
|
|
39
|
+
typeof size !== 'number' ||
|
|
40
|
+
!Number.isSafeInteger(size) ||
|
|
41
|
+
size < 0) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
entries.push({
|
|
45
|
+
fieldName,
|
|
46
|
+
relativePath,
|
|
47
|
+
size
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
return { terminalId: manifest.terminalId, entries };
|
|
51
|
+
}
|
|
52
|
+
class StreamingUploadSession {
|
|
53
|
+
destinationCwd;
|
|
54
|
+
entries;
|
|
55
|
+
limits;
|
|
56
|
+
targetsByField = new Map();
|
|
57
|
+
failuresByField = new Map();
|
|
58
|
+
resultsByField = new Map();
|
|
59
|
+
createdFiles = new Set();
|
|
60
|
+
actualBatchBytes = 0;
|
|
61
|
+
constructor(destinationCwd, entries, limits, targets, failures) {
|
|
62
|
+
this.destinationCwd = destinationCwd;
|
|
63
|
+
this.entries = entries;
|
|
64
|
+
this.limits = limits;
|
|
65
|
+
for (const target of targets) {
|
|
66
|
+
if (target.size > limits.uploadMaxFileBytes) {
|
|
67
|
+
this.failuresByField.set(target.fieldName, {
|
|
68
|
+
relativePath: target.relativePath,
|
|
69
|
+
status: 'failed',
|
|
70
|
+
error: 'file_too_large'
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
this.targetsByField.set(target.fieldName, target);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
for (const failure of failures) {
|
|
78
|
+
this.failuresByField.set(failure.fieldName, {
|
|
79
|
+
relativePath: failure.relativePath,
|
|
80
|
+
status: 'failed',
|
|
81
|
+
error: failure.error
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
async writeFile(fieldName, stream) {
|
|
86
|
+
if (this.resultsByField.has(fieldName)) {
|
|
87
|
+
await drain(stream);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const target = this.targetsByField.get(fieldName);
|
|
91
|
+
if (!target || this.failuresByField.has(fieldName)) {
|
|
92
|
+
await drain(stream);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const parentDir = path.dirname(target.absolutePath);
|
|
96
|
+
await mkdir(parentDir, { recursive: true });
|
|
97
|
+
if (!(await isContainedRealPath(this.destinationCwd, parentDir))) {
|
|
98
|
+
await drain(stream);
|
|
99
|
+
this.resultsByField.set(fieldName, {
|
|
100
|
+
relativePath: target.relativePath,
|
|
101
|
+
savedRelativePath: target.savedRelativePath,
|
|
102
|
+
status: 'failed',
|
|
103
|
+
error: 'unsafe_parent_path'
|
|
104
|
+
});
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const counter = this.createLimitCounter();
|
|
108
|
+
let createdPath = null;
|
|
109
|
+
let handle = null;
|
|
110
|
+
try {
|
|
111
|
+
handle = await open(target.absolutePath, 'wx');
|
|
112
|
+
createdPath = target.absolutePath;
|
|
113
|
+
await pipeline(stream, counter, handle.createWriteStream());
|
|
114
|
+
this.createdFiles.add(target.absolutePath);
|
|
115
|
+
this.resultsByField.set(fieldName, {
|
|
116
|
+
relativePath: target.relativePath,
|
|
117
|
+
savedRelativePath: target.savedRelativePath,
|
|
118
|
+
status: 'uploaded',
|
|
119
|
+
size: counter.bytesRead
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
if (!createdPath) {
|
|
124
|
+
await drain(stream).catch(() => undefined);
|
|
125
|
+
}
|
|
126
|
+
await handle?.close().catch(() => undefined);
|
|
127
|
+
if (createdPath) {
|
|
128
|
+
await unlink(createdPath).catch(() => undefined);
|
|
129
|
+
}
|
|
130
|
+
this.resultsByField.set(fieldName, {
|
|
131
|
+
relativePath: target.relativePath,
|
|
132
|
+
savedRelativePath: target.savedRelativePath,
|
|
133
|
+
status: 'failed',
|
|
134
|
+
error: uploadWriteErrorCode(error)
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
async abort() {
|
|
139
|
+
const createdFiles = Array.from(this.createdFiles);
|
|
140
|
+
this.createdFiles.clear();
|
|
141
|
+
await Promise.all(createdFiles.map((filePath) => unlink(filePath).catch(() => undefined)));
|
|
142
|
+
}
|
|
143
|
+
async finish() {
|
|
144
|
+
const results = this.entries.map((entry) => {
|
|
145
|
+
const result = this.resultsByField.get(entry.fieldName);
|
|
146
|
+
if (result) {
|
|
147
|
+
return result;
|
|
148
|
+
}
|
|
149
|
+
const failure = this.failuresByField.get(entry.fieldName);
|
|
150
|
+
if (failure) {
|
|
151
|
+
return failure;
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
relativePath: entry.relativePath,
|
|
155
|
+
status: 'failed',
|
|
156
|
+
error: 'missing_file'
|
|
157
|
+
};
|
|
158
|
+
});
|
|
159
|
+
const uploaded = results.filter((result) => result.status === 'uploaded').length;
|
|
160
|
+
return {
|
|
161
|
+
destinationCwd: this.destinationCwd,
|
|
162
|
+
uploaded,
|
|
163
|
+
failed: results.length - uploaded,
|
|
164
|
+
results
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
createLimitCounter() {
|
|
168
|
+
const limits = this.limits;
|
|
169
|
+
const session = this;
|
|
170
|
+
const counter = new Transform({
|
|
171
|
+
transform(chunk, _encoding, callback) {
|
|
172
|
+
const chunkBytes = typeof chunk === 'string' ? Buffer.byteLength(chunk) : chunk.length;
|
|
173
|
+
counter.bytesRead += chunkBytes;
|
|
174
|
+
session.actualBatchBytes += chunkBytes;
|
|
175
|
+
if (counter.bytesRead > limits.uploadMaxFileBytes) {
|
|
176
|
+
callback(new UploadRequestError(413, 'file_too_large'));
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
if (session.actualBatchBytes > limits.uploadMaxBatchBytes) {
|
|
180
|
+
callback(new UploadRequestError(413, 'batch_too_large'));
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
callback(null, chunk);
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
counter.bytesRead = 0;
|
|
187
|
+
return counter;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
function validateManifest(manifest, limits) {
|
|
191
|
+
if (manifest.entries.length > limits.uploadMaxFiles) {
|
|
192
|
+
throw new UploadRequestError(413, 'too_many_files');
|
|
193
|
+
}
|
|
194
|
+
const fieldNames = new Set();
|
|
195
|
+
for (const entry of manifest.entries) {
|
|
196
|
+
if (fieldNames.has(entry.fieldName)) {
|
|
197
|
+
throw new UploadRequestError(400, 'duplicate_upload_field');
|
|
198
|
+
}
|
|
199
|
+
fieldNames.add(entry.fieldName);
|
|
200
|
+
}
|
|
201
|
+
const batchBytes = manifest.entries.reduce((total, entry) => total + entry.size, 0);
|
|
202
|
+
if (batchBytes > limits.uploadMaxBatchBytes) {
|
|
203
|
+
throw new UploadRequestError(413, 'batch_too_large');
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
async function drain(stream) {
|
|
207
|
+
for await (const _chunk of stream) {
|
|
208
|
+
// Consume the stream so multipart parsing can continue.
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
function uploadWriteErrorCode(error) {
|
|
212
|
+
if (isNodeErrorWithCode(error, 'EEXIST')) {
|
|
213
|
+
return 'target_exists';
|
|
214
|
+
}
|
|
215
|
+
return error instanceof UploadRequestError ? error.code : 'write_failed';
|
|
216
|
+
}
|
|
217
|
+
async function isContainedRealPath(root, candidate) {
|
|
218
|
+
try {
|
|
219
|
+
const [rootRealPath, candidateRealPath] = await Promise.all([realpath(root), realpath(candidate)]);
|
|
220
|
+
const relative = path.relative(rootRealPath, candidateRealPath);
|
|
221
|
+
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
function isNodeErrorWithCode(error, code) {
|
|
228
|
+
return typeof error === 'object' && error !== null && 'code' in error && error.code === code;
|
|
229
|
+
}
|
|
230
|
+
//# sourceMappingURL=uploadService.js.map
|