blun-king-cli 9.1.58 → 9.1.61
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/LIESMICH.txt +1 -1
- package/README.md +1 -1
- package/bin/core-bootstrap.js +3 -6
- package/bin/launcher-mode.js +2 -0
- package/bin/launcher-runtime.js +203 -30
- package/bin/profile-runtime.cjs +264 -0
- package/bin/rate-limit-recovery-policy.cjs +34 -0
- package/bin/running-update.cjs +389 -0
- package/bin/standard-tools-bootstrap.js +45 -4
- package/bin/subagent-timeout-policy.cjs +79 -0
- package/bin/update-notice.js +24 -0
- package/blun.mjs +296 -109
- package/package.json +1 -1
- package/standard-tools/manifest.json +3 -3
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
|
|
6
|
+
const DEFAULT_PROFILE = 'default';
|
|
7
|
+
const PROFILE_NAME_PATTERN = /^[a-z0-9][a-z0-9_-]{0,31}$/u;
|
|
8
|
+
const RESERVED_SINGLE_ARGUMENTS = new Set([
|
|
9
|
+
'doctor',
|
|
10
|
+
'export',
|
|
11
|
+
'help',
|
|
12
|
+
'mcp',
|
|
13
|
+
'server',
|
|
14
|
+
'tools',
|
|
15
|
+
'update',
|
|
16
|
+
'upgrade',
|
|
17
|
+
'version',
|
|
18
|
+
]);
|
|
19
|
+
const LEGACY_PROFILE_ENTRIES = Object.freeze([
|
|
20
|
+
'channels',
|
|
21
|
+
'config.toml',
|
|
22
|
+
'mcp.json',
|
|
23
|
+
'plugins',
|
|
24
|
+
'session_index.jsonl',
|
|
25
|
+
'sessions',
|
|
26
|
+
'tui.toml',
|
|
27
|
+
]);
|
|
28
|
+
const MIGRATION_MARKER = '.legacy-profile-migrated-v1';
|
|
29
|
+
const SESSION_PATH_MIGRATION_MARKER = '.legacy-profile-session-paths-v2';
|
|
30
|
+
const SESSION_PATH_BACKUP_DIR = '.legacy-profile-session-paths-v2-backup';
|
|
31
|
+
|
|
32
|
+
function normalizeProfileName(value) {
|
|
33
|
+
const normalized = String(value || '').trim().toLowerCase();
|
|
34
|
+
if (!PROFILE_NAME_PATTERN.test(normalized)) {
|
|
35
|
+
throw new Error('Profilnamen duerfen nur Buchstaben, Zahlen, Unterstriche und Bindestriche enthalten.');
|
|
36
|
+
}
|
|
37
|
+
return normalized;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function parseProfileLaunchArgs(inputArgs, mode = 'blun') {
|
|
41
|
+
const args = Array.isArray(inputArgs) ? [...inputArgs] : [];
|
|
42
|
+
if (String(mode).toLowerCase() !== 'king') {
|
|
43
|
+
return { args, profileName: DEFAULT_PROFILE };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const profileFlagIndex = args.indexOf('--profile');
|
|
47
|
+
if (profileFlagIndex >= 0) {
|
|
48
|
+
if (profileFlagIndex + 1 >= args.length) {
|
|
49
|
+
throw new Error('--profile braucht einen Profilnamen.');
|
|
50
|
+
}
|
|
51
|
+
const profileName = normalizeProfileName(args[profileFlagIndex + 1]);
|
|
52
|
+
args.splice(profileFlagIndex, 2);
|
|
53
|
+
return { args, profileName };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (args.length === 1) {
|
|
57
|
+
const candidate = String(args[0] || '');
|
|
58
|
+
if (!candidate.startsWith('-') && !RESERVED_SINGLE_ARGUMENTS.has(candidate.toLowerCase())) {
|
|
59
|
+
return { args: [], profileName: normalizeProfileName(candidate) };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (args.length === 2 && args.includes('-c')) {
|
|
64
|
+
const candidate = args.find((arg) => arg !== '-c');
|
|
65
|
+
if (candidate && !candidate.startsWith('-')) {
|
|
66
|
+
return { args: ['-c'], profileName: normalizeProfileName(candidate) };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return { args, profileName: DEFAULT_PROFILE };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function resolveProfilePaths(sharedHome, profileName, pathImpl = path) {
|
|
74
|
+
const normalized = normalizeProfileName(profileName);
|
|
75
|
+
const home = pathImpl.join(sharedHome, 'profile', normalized);
|
|
76
|
+
return Object.freeze({
|
|
77
|
+
profileName: normalized,
|
|
78
|
+
sharedHome,
|
|
79
|
+
home,
|
|
80
|
+
channels: pathImpl.join(home, 'channels'),
|
|
81
|
+
sessions: pathImpl.join(home, 'sessions'),
|
|
82
|
+
plugins: pathImpl.join(home, 'plugins'),
|
|
83
|
+
config: pathImpl.join(home, 'config.toml'),
|
|
84
|
+
mcp: pathImpl.join(home, 'mcp.json'),
|
|
85
|
+
sessionIndex: pathImpl.join(home, 'session_index.jsonl'),
|
|
86
|
+
credentials: pathImpl.join(sharedHome, 'credentials'),
|
|
87
|
+
oauth: pathImpl.join(sharedHome, 'oauth'),
|
|
88
|
+
deviceId: pathImpl.join(sharedHome, 'device_id'),
|
|
89
|
+
skills: pathImpl.join(sharedHome, 'skills'),
|
|
90
|
+
secure: pathImpl.join(sharedHome, 'secure'),
|
|
91
|
+
persona: pathImpl.join(sharedHome, 'persona.json'),
|
|
92
|
+
globalLog: pathImpl.join(sharedHome, 'logs', 'blun.log'),
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function copyLegacyEntry(source, target, fsImpl) {
|
|
97
|
+
if (!fsImpl.existsSync(source) || fsImpl.existsSync(target)) return;
|
|
98
|
+
const stat = fsImpl.lstatSync(source);
|
|
99
|
+
if (stat.isSymbolicLink()) return;
|
|
100
|
+
if (stat.isDirectory()) {
|
|
101
|
+
fsImpl.cpSync(source, target, { recursive: true, errorOnExist: false });
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (stat.isFile()) fsImpl.copyFileSync(source, target);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function relativePathInside(parent, child) {
|
|
108
|
+
const relative = path.relative(path.resolve(parent), path.resolve(child));
|
|
109
|
+
if (!relative || relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
return relative;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function replaceTextFileIfUnchanged(target, original, replacement, fsImpl) {
|
|
116
|
+
if (fsImpl.readFileSync(target, 'utf8') !== original) {
|
|
117
|
+
throw new Error(`Sitzungsdatei wurde waehrend der Migration geaendert: ${target}`);
|
|
118
|
+
}
|
|
119
|
+
const temporary = `${target}.migration-${process.pid}-${Date.now()}.tmp`;
|
|
120
|
+
fsImpl.writeFileSync(temporary, replacement, 'utf8');
|
|
121
|
+
try {
|
|
122
|
+
fsImpl.renameSync(temporary, target);
|
|
123
|
+
} catch (error) {
|
|
124
|
+
try {
|
|
125
|
+
fsImpl.rmSync(temporary, { force: true });
|
|
126
|
+
} catch {}
|
|
127
|
+
throw error;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function backupFile(source, target, fsImpl) {
|
|
132
|
+
if (fsImpl.existsSync(target)) return;
|
|
133
|
+
fsImpl.mkdirSync(path.dirname(target), { recursive: true });
|
|
134
|
+
fsImpl.copyFileSync(source, target);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function rewriteSessionState(statePath, legacySessionDir, profileSessionDir, backupRoot, relativeSessionDir, fsImpl) {
|
|
138
|
+
if (!fsImpl.existsSync(statePath)) return false;
|
|
139
|
+
const original = fsImpl.readFileSync(statePath, 'utf8');
|
|
140
|
+
let state;
|
|
141
|
+
try {
|
|
142
|
+
state = JSON.parse(original);
|
|
143
|
+
} catch {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
if (!state || typeof state !== 'object' || Array.isArray(state) || !state.agents || typeof state.agents !== 'object') {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
let changed = false;
|
|
151
|
+
const agents = {};
|
|
152
|
+
for (const [agentId, value] of Object.entries(state.agents)) {
|
|
153
|
+
if (!value || typeof value !== 'object' || Array.isArray(value) || typeof value.homedir !== 'string') {
|
|
154
|
+
agents[agentId] = value;
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
const relativeAgentPath = relativePathInside(legacySessionDir, value.homedir);
|
|
158
|
+
if (relativeAgentPath === null) {
|
|
159
|
+
agents[agentId] = value;
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
agents[agentId] = {
|
|
163
|
+
...value,
|
|
164
|
+
homedir: path.join(profileSessionDir, relativeAgentPath),
|
|
165
|
+
};
|
|
166
|
+
changed = true;
|
|
167
|
+
}
|
|
168
|
+
if (!changed) return false;
|
|
169
|
+
|
|
170
|
+
backupFile(statePath, path.join(backupRoot, 'sessions', relativeSessionDir, 'state.json'), fsImpl);
|
|
171
|
+
const replacement = `${JSON.stringify({ ...state, agents }, null, 2)}\n`;
|
|
172
|
+
replaceTextFileIfUnchanged(statePath, original, replacement, fsImpl);
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function repairDefaultProfileSessionPaths(sharedHome, fsImpl = fs, options = {}) {
|
|
177
|
+
const paths = resolveProfilePaths(sharedHome, DEFAULT_PROFILE);
|
|
178
|
+
const marker = path.join(paths.home, SESSION_PATH_MIGRATION_MARKER);
|
|
179
|
+
const filteredWorkDir = options.workDir === undefined ? null : path.resolve(options.workDir);
|
|
180
|
+
if (filteredWorkDir === null && fsImpl.existsSync(marker)) return paths;
|
|
181
|
+
if (!fsImpl.existsSync(paths.sessionIndex)) {
|
|
182
|
+
if (filteredWorkDir === null) fsImpl.writeFileSync(marker, 'legacy session paths repaired once\n', 'utf8');
|
|
183
|
+
return paths;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const legacySessions = path.join(sharedHome, 'sessions');
|
|
187
|
+
const backupRoot = path.join(paths.home, SESSION_PATH_BACKUP_DIR);
|
|
188
|
+
const original = fsImpl.readFileSync(paths.sessionIndex, 'utf8');
|
|
189
|
+
const output = [];
|
|
190
|
+
let changed = false;
|
|
191
|
+
for (const line of original.split(/\r?\n/u)) {
|
|
192
|
+
if (line.trim() === '') {
|
|
193
|
+
output.push(line);
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
let entry;
|
|
197
|
+
try {
|
|
198
|
+
entry = JSON.parse(line);
|
|
199
|
+
} catch {
|
|
200
|
+
output.push(line);
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
if (!entry || typeof entry !== 'object' || typeof entry.sessionId !== 'string'
|
|
204
|
+
|| typeof entry.sessionDir !== 'string' || typeof entry.workDir !== 'string'
|
|
205
|
+
|| (filteredWorkDir !== null && path.resolve(entry.workDir) !== filteredWorkDir)) {
|
|
206
|
+
output.push(line);
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
const relativeSessionDir = relativePathInside(legacySessions, entry.sessionDir);
|
|
210
|
+
if (relativeSessionDir === null || path.basename(relativeSessionDir) !== entry.sessionId) {
|
|
211
|
+
output.push(line);
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
const profileSessionDir = path.join(paths.sessions, relativeSessionDir);
|
|
215
|
+
if (!fsImpl.existsSync(profileSessionDir)) {
|
|
216
|
+
output.push(line);
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
rewriteSessionState(
|
|
221
|
+
path.join(profileSessionDir, 'state.json'),
|
|
222
|
+
path.resolve(entry.sessionDir),
|
|
223
|
+
profileSessionDir,
|
|
224
|
+
backupRoot,
|
|
225
|
+
relativeSessionDir,
|
|
226
|
+
fsImpl,
|
|
227
|
+
);
|
|
228
|
+
output.push(JSON.stringify({ ...entry, sessionDir: profileSessionDir }));
|
|
229
|
+
changed = true;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (changed) {
|
|
233
|
+
backupFile(paths.sessionIndex, path.join(backupRoot, 'session_index.jsonl'), fsImpl);
|
|
234
|
+
replaceTextFileIfUnchanged(paths.sessionIndex, original, output.join('\n'), fsImpl);
|
|
235
|
+
}
|
|
236
|
+
if (filteredWorkDir === null) fsImpl.writeFileSync(marker, 'legacy session paths repaired once\n', 'utf8');
|
|
237
|
+
return paths;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function seedDefaultProfileFromLegacy(sharedHome, fsImpl = fs) {
|
|
241
|
+
const paths = resolveProfilePaths(sharedHome, DEFAULT_PROFILE);
|
|
242
|
+
const marker = path.join(paths.home, MIGRATION_MARKER);
|
|
243
|
+
fsImpl.mkdirSync(paths.home, { recursive: true });
|
|
244
|
+
if (!fsImpl.existsSync(marker)) {
|
|
245
|
+
for (const entry of LEGACY_PROFILE_ENTRIES) {
|
|
246
|
+
copyLegacyEntry(path.join(sharedHome, entry), path.join(paths.home, entry), fsImpl);
|
|
247
|
+
}
|
|
248
|
+
try {
|
|
249
|
+
fsImpl.writeFileSync(marker, 'legacy profile state copied once\n', { encoding: 'utf8', flag: 'wx' });
|
|
250
|
+
} catch (error) {
|
|
251
|
+
if (error?.code !== 'EEXIST') throw error;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return repairDefaultProfileSessionPaths(sharedHome, fsImpl);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
module.exports = {
|
|
258
|
+
DEFAULT_PROFILE,
|
|
259
|
+
normalizeProfileName,
|
|
260
|
+
parseProfileLaunchArgs,
|
|
261
|
+
repairDefaultProfileSessionPaths,
|
|
262
|
+
resolveProfilePaths,
|
|
263
|
+
seedDefaultProfileFromLegacy,
|
|
264
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const MAX_AUTOMATIC_RATE_LIMIT_RESUMES = 3;
|
|
4
|
+
const RATE_LIMIT_RETRY_BASE_MS = 60_000;
|
|
5
|
+
const RATE_LIMIT_COMPACTION_THRESHOLD_TOKENS = 64_000;
|
|
6
|
+
|
|
7
|
+
function isImmediateGenerateRetryAllowed(statusCode) {
|
|
8
|
+
return statusCode !== 429 && [500, 502, 503, 504].includes(statusCode);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function rateLimitRetryDelayMs(retryCount) {
|
|
12
|
+
if (!Number.isInteger(retryCount) || retryCount < 1) {
|
|
13
|
+
throw new Error("retryCount must be a positive integer.");
|
|
14
|
+
}
|
|
15
|
+
return RATE_LIMIT_RETRY_BASE_MS * (2 ** (retryCount - 1));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function shouldStopAutomaticRateLimitResume(retryCount) {
|
|
19
|
+
return retryCount >= MAX_AUTOMATIC_RATE_LIMIT_RESUMES;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function shouldCompactBeforeRateLimitResume(estimatedRequestTokens) {
|
|
23
|
+
return Number.isFinite(estimatedRequestTokens)
|
|
24
|
+
&& estimatedRequestTokens >= RATE_LIMIT_COMPACTION_THRESHOLD_TOKENS;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
module.exports = {
|
|
28
|
+
MAX_AUTOMATIC_RATE_LIMIT_RESUMES,
|
|
29
|
+
RATE_LIMIT_RETRY_BASE_MS,
|
|
30
|
+
isImmediateGenerateRetryAllowed,
|
|
31
|
+
rateLimitRetryDelayMs,
|
|
32
|
+
shouldCompactBeforeRateLimitResume,
|
|
33
|
+
shouldStopAutomaticRateLimitResume,
|
|
34
|
+
};
|
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { createHash, randomUUID } = require('node:crypto');
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const https = require('node:https');
|
|
6
|
+
const path = require('node:path');
|
|
7
|
+
const { spawn } = require('node:child_process');
|
|
8
|
+
|
|
9
|
+
const { compareSemver, requestTrustedJson } = require('./update-notice');
|
|
10
|
+
const { ensurePrivateDirectory, securePrivateFile, writePrivateFile } = require('./private-paths');
|
|
11
|
+
|
|
12
|
+
const PACKAGE_NAME = 'blun-king-cli';
|
|
13
|
+
const MANIFEST_URL = 'https://chat.blun.ai/blun-code-version.json';
|
|
14
|
+
const REGISTRY_URL = 'https://registry.npmjs.org/blun-king-cli';
|
|
15
|
+
const ACTIVE_RUNTIME_FILE = 'active-runtime.json';
|
|
16
|
+
const RUNNING_UPDATE_HANDOFF_EXIT_CODE = 76;
|
|
17
|
+
const RUNNING_UPDATE_PREPARED_MESSAGE = 'blun-running-update-prepared';
|
|
18
|
+
const RUNNING_UPDATE_HANDOFF_MESSAGE = 'blun-running-update-handoff';
|
|
19
|
+
const RUNTIME_READY_MESSAGE = 'blun-runtime-session-ready';
|
|
20
|
+
const MAX_TARBALL_BYTES = 128 * 1024 * 1024;
|
|
21
|
+
|
|
22
|
+
function ownData(value, key) {
|
|
23
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) return undefined;
|
|
24
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
25
|
+
return descriptor && Object.hasOwn(descriptor, 'value') ? descriptor.value : undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function validIntegrity(value) {
|
|
29
|
+
if (typeof value !== 'string') return false;
|
|
30
|
+
const match = /^sha512-([A-Za-z0-9+/]+={0,2})$/u.exec(value);
|
|
31
|
+
if (!match) return false;
|
|
32
|
+
try {
|
|
33
|
+
return Buffer.from(match[1], 'base64').length === 64;
|
|
34
|
+
} catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function selectRunningUpdateTarget(currentVersion, manifest) {
|
|
40
|
+
const name = ownData(manifest, 'name');
|
|
41
|
+
const version = ownData(manifest, 'latest');
|
|
42
|
+
const integrity = ownData(manifest, 'integrity');
|
|
43
|
+
const releasedAt = ownData(manifest, 'releasedAt');
|
|
44
|
+
if (name !== PACKAGE_NAME
|
|
45
|
+
|| typeof version !== 'string'
|
|
46
|
+
|| compareSemver(currentVersion, version) !== -1
|
|
47
|
+
|| !validIntegrity(integrity)
|
|
48
|
+
|| typeof releasedAt !== 'string'
|
|
49
|
+
|| !Number.isFinite(Date.parse(releasedAt))) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
return Object.freeze({ version, integrity });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function isSafeRuntimeBoundary(state) {
|
|
56
|
+
return state?.isShuttingDown === false
|
|
57
|
+
&& state.streamingPhase === 'idle'
|
|
58
|
+
&& state.isCompacting === false
|
|
59
|
+
&& state.queuedMessages === 0
|
|
60
|
+
&& state.activeToolCalls === 0
|
|
61
|
+
&& state.shellCommands === 0
|
|
62
|
+
&& state.queueCommandRunning === false;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function resumeArgsForHandoff(args, sessionId) {
|
|
66
|
+
const filtered = [];
|
|
67
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
68
|
+
const arg = args[index];
|
|
69
|
+
if (arg === '-c' || arg === '--continue') continue;
|
|
70
|
+
if (arg === '-r' || arg === '--resume' || arg === '--session') {
|
|
71
|
+
index += 1;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (/^(?:--resume|--session)=/u.test(arg)) continue;
|
|
75
|
+
filtered.push(arg);
|
|
76
|
+
}
|
|
77
|
+
return [...filtered, '--resume', sessionId];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function handoffRuntime(options) {
|
|
81
|
+
const handoffArgs = resumeArgsForHandoff(options.args, options.sessionId);
|
|
82
|
+
if (!await options.probeTarget(options.target)) {
|
|
83
|
+
return { kind: 'probe-failed', runtime: options.previous };
|
|
84
|
+
}
|
|
85
|
+
await options.stopOld();
|
|
86
|
+
const targetCore = await options.startCore({
|
|
87
|
+
packageRoot: options.target.packageRoot,
|
|
88
|
+
args: handoffArgs,
|
|
89
|
+
});
|
|
90
|
+
if (targetCore.ready === true) {
|
|
91
|
+
await options.activateTarget(options.target);
|
|
92
|
+
return { kind: 'activated', runtime: options.target, core: targetCore };
|
|
93
|
+
}
|
|
94
|
+
const fallbackCore = await options.startCore({
|
|
95
|
+
packageRoot: options.previous.packageRoot,
|
|
96
|
+
args: handoffArgs,
|
|
97
|
+
});
|
|
98
|
+
return { kind: 'rolled-back', runtime: options.previous, core: fallbackCore };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function releasesRoot(sharedHome) {
|
|
102
|
+
return path.join(path.resolve(sharedHome), 'updates', 'releases');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function activeRuntimePath(sharedHome) {
|
|
106
|
+
return path.join(path.resolve(sharedHome), 'updates', ACTIVE_RUNTIME_FILE);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function pathWithin(root, candidate) {
|
|
110
|
+
const relative = path.relative(root, candidate);
|
|
111
|
+
return relative === '' || (relative !== '..'
|
|
112
|
+
&& !relative.startsWith(`..${path.sep}`)
|
|
113
|
+
&& !path.isAbsolute(relative));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function verifyRuntimePackage(packageRoot, version, options = {}) {
|
|
117
|
+
try {
|
|
118
|
+
const root = path.resolve(packageRoot);
|
|
119
|
+
const allowedRoot = path.resolve(options.allowedRoot || releasesRoot(options.sharedHome));
|
|
120
|
+
if (!pathWithin(allowedRoot, root)) return false;
|
|
121
|
+
const manifestPath = path.join(root, 'package.json');
|
|
122
|
+
const bundlePath = path.join(root, 'blun.mjs');
|
|
123
|
+
const bootstrapPath = path.join(root, 'bin', 'core-bootstrap.js');
|
|
124
|
+
for (const filePath of [manifestPath, bundlePath, bootstrapPath]) {
|
|
125
|
+
const stat = fs.lstatSync(filePath);
|
|
126
|
+
if (!stat.isFile() || stat.isSymbolicLink()) return false;
|
|
127
|
+
}
|
|
128
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
129
|
+
return ownData(manifest, 'name') === PACKAGE_NAME && ownData(manifest, 'version') === version;
|
|
130
|
+
} catch {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function readActiveRuntime(sharedHome) {
|
|
136
|
+
try {
|
|
137
|
+
const filePath = activeRuntimePath(sharedHome);
|
|
138
|
+
const stat = fs.lstatSync(filePath);
|
|
139
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 16 * 1024) return null;
|
|
140
|
+
const record = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
141
|
+
const version = ownData(record, 'version');
|
|
142
|
+
const packageRoot = ownData(record, 'packageRoot');
|
|
143
|
+
const integrity = ownData(record, 'integrity');
|
|
144
|
+
if (typeof version !== 'string'
|
|
145
|
+
|| typeof packageRoot !== 'string'
|
|
146
|
+
|| !path.isAbsolute(packageRoot)
|
|
147
|
+
|| !validIntegrity(integrity)
|
|
148
|
+
|| !verifyRuntimePackage(packageRoot, version, { sharedHome })) {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
return Object.freeze({ version, packageRoot: path.resolve(packageRoot), integrity });
|
|
152
|
+
} catch {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function activateRuntime(sharedHome, target) {
|
|
158
|
+
if (!verifyRuntimePackage(target.packageRoot, target.version, { sharedHome })
|
|
159
|
+
|| !validIntegrity(target.integrity)) {
|
|
160
|
+
throw new Error('RUNNING_UPDATE_TARGET_INVALID');
|
|
161
|
+
}
|
|
162
|
+
const updateRoot = path.join(path.resolve(sharedHome), 'updates');
|
|
163
|
+
ensurePrivateDirectory(path.resolve(sharedHome));
|
|
164
|
+
ensurePrivateDirectory(updateRoot);
|
|
165
|
+
const filePath = activeRuntimePath(sharedHome);
|
|
166
|
+
const temporaryPath = path.join(updateRoot, `.${ACTIVE_RUNTIME_FILE}.${process.pid}.${randomUUID()}.tmp`);
|
|
167
|
+
try {
|
|
168
|
+
writePrivateFile(temporaryPath, `${JSON.stringify({
|
|
169
|
+
version: target.version,
|
|
170
|
+
packageRoot: path.resolve(target.packageRoot),
|
|
171
|
+
integrity: target.integrity,
|
|
172
|
+
activatedAt: new Date().toISOString(),
|
|
173
|
+
})}\n`);
|
|
174
|
+
fs.renameSync(temporaryPath, filePath);
|
|
175
|
+
securePrivateFile(filePath);
|
|
176
|
+
} catch (error) {
|
|
177
|
+
fs.rmSync(temporaryPath, { force: true });
|
|
178
|
+
throw error;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function registryRelease(registry, version) {
|
|
183
|
+
const versions = ownData(registry, 'versions');
|
|
184
|
+
const entry = ownData(versions, version);
|
|
185
|
+
const dist = ownData(entry, 'dist');
|
|
186
|
+
const integrity = ownData(dist, 'integrity');
|
|
187
|
+
const tarball = ownData(dist, 'tarball');
|
|
188
|
+
if (!validIntegrity(integrity) || typeof tarball !== 'string') return null;
|
|
189
|
+
let url;
|
|
190
|
+
try {
|
|
191
|
+
url = new URL(tarball);
|
|
192
|
+
} catch {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
if (url.protocol !== 'https:'
|
|
196
|
+
|| url.hostname !== 'registry.npmjs.org'
|
|
197
|
+
|| url.username !== ''
|
|
198
|
+
|| url.password !== '') {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
return { integrity, tarball: url.href };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function downloadTarball(url, destination, options = {}) {
|
|
205
|
+
const httpsImpl = options.httpsImpl || https;
|
|
206
|
+
return new Promise((resolve, reject) => {
|
|
207
|
+
const request = httpsImpl.get(url, {
|
|
208
|
+
headers: { 'user-agent': 'blun-king-running-update' },
|
|
209
|
+
timeout: options.timeoutMs || 30_000,
|
|
210
|
+
}, (response) => {
|
|
211
|
+
if (response.statusCode !== 200) {
|
|
212
|
+
response.resume();
|
|
213
|
+
reject(new Error(`RUNNING_UPDATE_DOWNLOAD_${String(response.statusCode)}`));
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
let bytes = 0;
|
|
217
|
+
const handle = fs.openSync(destination, 'wx', 0o600);
|
|
218
|
+
let settled = false;
|
|
219
|
+
const fail = (error) => {
|
|
220
|
+
if (settled) return;
|
|
221
|
+
settled = true;
|
|
222
|
+
try { fs.closeSync(handle); } catch {}
|
|
223
|
+
fs.rmSync(destination, { force: true });
|
|
224
|
+
reject(error);
|
|
225
|
+
};
|
|
226
|
+
response.on('data', (chunk) => {
|
|
227
|
+
bytes += chunk.length;
|
|
228
|
+
if (bytes > MAX_TARBALL_BYTES) {
|
|
229
|
+
response.destroy(new Error('RUNNING_UPDATE_TARBALL_TOO_LARGE'));
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
try {
|
|
233
|
+
fs.writeSync(handle, chunk);
|
|
234
|
+
} catch (error) {
|
|
235
|
+
response.destroy(error);
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
response.once('error', fail);
|
|
239
|
+
response.once('end', () => {
|
|
240
|
+
if (settled) return;
|
|
241
|
+
settled = true;
|
|
242
|
+
fs.fsyncSync(handle);
|
|
243
|
+
fs.closeSync(handle);
|
|
244
|
+
resolve();
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
request.once('timeout', () => request.destroy(new Error('RUNNING_UPDATE_DOWNLOAD_TIMEOUT')));
|
|
248
|
+
request.once('error', reject);
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function resolveNpmCliPath(execPath = process.execPath) {
|
|
253
|
+
const executableDirectory = path.dirname(path.resolve(execPath));
|
|
254
|
+
const candidates = [
|
|
255
|
+
path.join(executableDirectory, 'node_modules', 'npm', 'bin', 'npm-cli.js'),
|
|
256
|
+
path.resolve(executableDirectory, '..', 'lib', 'node_modules', 'npm', 'bin', 'npm-cli.js'),
|
|
257
|
+
path.resolve(executableDirectory, '..', 'node_modules', 'npm', 'bin', 'npm-cli.js'),
|
|
258
|
+
path.resolve(__dirname, '..', '..', 'npm', 'bin', 'npm-cli.js'),
|
|
259
|
+
];
|
|
260
|
+
for (const candidate of candidates) {
|
|
261
|
+
try {
|
|
262
|
+
const resolved = fs.realpathSync(candidate);
|
|
263
|
+
if (fs.statSync(resolved).isFile() && path.basename(resolved) === 'npm-cli.js') return resolved;
|
|
264
|
+
} catch {}
|
|
265
|
+
}
|
|
266
|
+
throw new Error('TRUSTED_NPM_CLI_NOT_FOUND');
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function runProcess(command, args, options = {}) {
|
|
270
|
+
return new Promise((resolve, reject) => {
|
|
271
|
+
const child = (options.spawnImpl || spawn)(command, args, {
|
|
272
|
+
cwd: options.cwd,
|
|
273
|
+
env: options.env,
|
|
274
|
+
stdio: options.stdio || 'ignore',
|
|
275
|
+
windowsHide: true,
|
|
276
|
+
});
|
|
277
|
+
child.once('error', reject);
|
|
278
|
+
child.once('exit', (code, signal) => {
|
|
279
|
+
if (code === 0) resolve();
|
|
280
|
+
else reject(new Error(`RUNNING_UPDATE_PROCESS_FAILED:${code ?? signal ?? 'unknown'}`));
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async function defaultInstallTarball(tarballPath, installPrefix, options = {}) {
|
|
286
|
+
const npmCliPath = resolveNpmCliPath(options.execPath);
|
|
287
|
+
const env = {
|
|
288
|
+
...process.env,
|
|
289
|
+
TEMP: installPrefix,
|
|
290
|
+
TMP: installPrefix,
|
|
291
|
+
TMPDIR: installPrefix,
|
|
292
|
+
npm_config_registry: 'https://registry.npmjs.org/',
|
|
293
|
+
npm_config_strict_ssl: 'true',
|
|
294
|
+
};
|
|
295
|
+
await runProcess(options.execPath || process.execPath, [
|
|
296
|
+
npmCliPath,
|
|
297
|
+
'install',
|
|
298
|
+
`--prefix=${installPrefix}`,
|
|
299
|
+
'--ignore-scripts=false',
|
|
300
|
+
'--package-lock=false',
|
|
301
|
+
'--audit=false',
|
|
302
|
+
'--fund=false',
|
|
303
|
+
tarballPath,
|
|
304
|
+
], { cwd: installPrefix, env, spawnImpl: options.spawnImpl });
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
async function defaultProbeRuntime(packageRoot, options = {}) {
|
|
308
|
+
try {
|
|
309
|
+
await runProcess(options.execPath || process.execPath, ['--check', path.join(packageRoot, 'blun.mjs')], options);
|
|
310
|
+
await runProcess(options.execPath || process.execPath, ['--check', path.join(packageRoot, 'bin', 'core-bootstrap.js')], options);
|
|
311
|
+
return true;
|
|
312
|
+
} catch {
|
|
313
|
+
return false;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
async function prepareRunningUpdate(options) {
|
|
318
|
+
if (String(options.env?.BLUN_NO_AUTO_UPDATE || '') === '1') return null;
|
|
319
|
+
const manifest = await (options.loadManifest
|
|
320
|
+
? options.loadManifest()
|
|
321
|
+
: requestTrustedJson(MANIFEST_URL));
|
|
322
|
+
const target = selectRunningUpdateTarget(options.currentVersion, manifest);
|
|
323
|
+
if (target === null) return null;
|
|
324
|
+
const registry = await (options.loadRegistry
|
|
325
|
+
? options.loadRegistry()
|
|
326
|
+
: requestTrustedJson(REGISTRY_URL));
|
|
327
|
+
const release = registryRelease(registry, target.version);
|
|
328
|
+
if (release === null || release.integrity !== target.integrity) {
|
|
329
|
+
throw new Error('RUNNING_UPDATE_INTEGRITY_SOURCE_MISMATCH');
|
|
330
|
+
}
|
|
331
|
+
const digest = createHash('sha256').update(target.integrity).digest('hex').slice(0, 16);
|
|
332
|
+
const releaseDirectory = path.join(releasesRoot(options.sharedHome), target.version, digest);
|
|
333
|
+
const packageRoot = path.join(releaseDirectory, 'node_modules', PACKAGE_NAME);
|
|
334
|
+
if (verifyRuntimePackage(packageRoot, target.version, { sharedHome: options.sharedHome })) {
|
|
335
|
+
return Object.freeze({ ...target, packageRoot });
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const updateRoot = path.join(path.resolve(options.sharedHome), 'updates');
|
|
339
|
+
const stagingRoot = path.join(updateRoot, 'staging');
|
|
340
|
+
ensurePrivateDirectory(path.resolve(options.sharedHome));
|
|
341
|
+
ensurePrivateDirectory(updateRoot);
|
|
342
|
+
ensurePrivateDirectory(stagingRoot);
|
|
343
|
+
const temporaryRoot = fs.mkdtempSync(path.join(stagingRoot, 'runtime-'));
|
|
344
|
+
ensurePrivateDirectory(temporaryRoot);
|
|
345
|
+
const tarballPath = path.join(temporaryRoot, `${PACKAGE_NAME}.tgz`);
|
|
346
|
+
const installPrefix = path.join(temporaryRoot, 'install');
|
|
347
|
+
ensurePrivateDirectory(installPrefix);
|
|
348
|
+
try {
|
|
349
|
+
await (options.downloadTarball || downloadTarball)(release.tarball, tarballPath);
|
|
350
|
+
const actualIntegrity = `sha512-${createHash('sha512').update(fs.readFileSync(tarballPath)).digest('base64')}`;
|
|
351
|
+
if (actualIntegrity !== target.integrity) throw new Error('RUNNING_UPDATE_TARBALL_INTEGRITY_FAILED');
|
|
352
|
+
await (options.installTarball || defaultInstallTarball)(tarballPath, installPrefix, options);
|
|
353
|
+
const installedRoot = path.join(installPrefix, 'node_modules', PACKAGE_NAME);
|
|
354
|
+
if (!verifyRuntimePackage(installedRoot, target.version, { allowedRoot: installPrefix })) {
|
|
355
|
+
throw new Error('RUNNING_UPDATE_PACKAGE_INVALID');
|
|
356
|
+
}
|
|
357
|
+
if (!await (options.probeRuntime || defaultProbeRuntime)(installedRoot, options)) {
|
|
358
|
+
throw new Error('RUNNING_UPDATE_PROBE_FAILED');
|
|
359
|
+
}
|
|
360
|
+
ensurePrivateDirectory(releasesRoot(options.sharedHome));
|
|
361
|
+
ensurePrivateDirectory(path.dirname(releaseDirectory));
|
|
362
|
+
if (fs.existsSync(releaseDirectory)) fs.rmSync(releaseDirectory, { recursive: true, force: true });
|
|
363
|
+
fs.renameSync(installPrefix, releaseDirectory);
|
|
364
|
+
if (!verifyRuntimePackage(packageRoot, target.version, { sharedHome: options.sharedHome })) {
|
|
365
|
+
throw new Error('RUNNING_UPDATE_ACTIVATION_PACKAGE_INVALID');
|
|
366
|
+
}
|
|
367
|
+
return Object.freeze({ ...target, packageRoot });
|
|
368
|
+
} finally {
|
|
369
|
+
fs.rmSync(temporaryRoot, { recursive: true, force: true });
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
module.exports = {
|
|
374
|
+
ACTIVE_RUNTIME_FILE,
|
|
375
|
+
RUNNING_UPDATE_HANDOFF_EXIT_CODE,
|
|
376
|
+
RUNNING_UPDATE_HANDOFF_MESSAGE,
|
|
377
|
+
RUNNING_UPDATE_PREPARED_MESSAGE,
|
|
378
|
+
RUNTIME_READY_MESSAGE,
|
|
379
|
+
activateRuntime,
|
|
380
|
+
activeRuntimePath,
|
|
381
|
+
defaultProbeRuntime,
|
|
382
|
+
handoffRuntime,
|
|
383
|
+
isSafeRuntimeBoundary,
|
|
384
|
+
prepareRunningUpdate,
|
|
385
|
+
readActiveRuntime,
|
|
386
|
+
resumeArgsForHandoff,
|
|
387
|
+
selectRunningUpdateTarget,
|
|
388
|
+
verifyRuntimePackage,
|
|
389
|
+
};
|