blun-king-cli 9.1.60 → 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-runtime.js +135 -12
- package/bin/profile-runtime.cjs +145 -9
- package/bin/running-update.cjs +389 -0
- package/bin/update-notice.js +24 -0
- package/blun.mjs +89 -19
- package/package.json +1 -1
package/LIESMICH.txt
CHANGED
package/README.md
CHANGED
package/bin/core-bootstrap.js
CHANGED
|
@@ -5,6 +5,7 @@ const path = require('node:path');
|
|
|
5
5
|
const { pathToFileURL } = require('node:url');
|
|
6
6
|
|
|
7
7
|
const { acquireSharedRuntimeLease } = require('./update-lease');
|
|
8
|
+
const { RUNTIME_READY_MESSAGE } = require('./running-update.cjs');
|
|
8
9
|
|
|
9
10
|
const CORE_LOADED_MESSAGE = 'blun-core-bootstrap-loaded';
|
|
10
11
|
|
|
@@ -31,11 +32,7 @@ async function runCoreBootstrap() {
|
|
|
31
32
|
|
|
32
33
|
if (process.connected) {
|
|
33
34
|
try {
|
|
34
|
-
process.send({ type: CORE_LOADED_MESSAGE }
|
|
35
|
-
try {
|
|
36
|
-
process.disconnect();
|
|
37
|
-
} catch {}
|
|
38
|
-
});
|
|
35
|
+
process.send({ type: CORE_LOADED_MESSAGE });
|
|
39
36
|
} catch {}
|
|
40
37
|
}
|
|
41
38
|
}
|
|
@@ -49,4 +46,4 @@ if (require.main === module) {
|
|
|
49
46
|
});
|
|
50
47
|
}
|
|
51
48
|
|
|
52
|
-
module.exports = { CORE_LOADED_MESSAGE, runCoreBootstrap };
|
|
49
|
+
module.exports = { CORE_LOADED_MESSAGE, RUNTIME_READY_MESSAGE, runCoreBootstrap };
|
package/bin/launcher-runtime.js
CHANGED
|
@@ -21,11 +21,21 @@ const {
|
|
|
21
21
|
seedStandardDesignSkill,
|
|
22
22
|
seedStandardTools,
|
|
23
23
|
} = require('./standard-tools-bootstrap');
|
|
24
|
-
const { CORE_LOADED_MESSAGE } = require('./core-bootstrap');
|
|
24
|
+
const { CORE_LOADED_MESSAGE, RUNTIME_READY_MESSAGE } = require('./core-bootstrap');
|
|
25
25
|
const { prepareManagedNodeRuntime } = require('./node-runtime');
|
|
26
26
|
const { repairConfiguredNativeModules } = require('./native-module-repair');
|
|
27
27
|
const { acquireSharedRuntimeLease, tryAcquireUpdateLease } = require('./update-lease');
|
|
28
|
-
const { runExplicitUpdate, runUpdateNotice } = require('./update-notice');
|
|
28
|
+
const { compareSemver, runExplicitUpdate, runUpdateNotice } = require('./update-notice');
|
|
29
|
+
const {
|
|
30
|
+
RUNNING_UPDATE_HANDOFF_EXIT_CODE,
|
|
31
|
+
RUNNING_UPDATE_HANDOFF_MESSAGE,
|
|
32
|
+
RUNNING_UPDATE_PREPARED_MESSAGE,
|
|
33
|
+
activateRuntime,
|
|
34
|
+
handoffRuntime,
|
|
35
|
+
prepareRunningUpdate,
|
|
36
|
+
readActiveRuntime,
|
|
37
|
+
resumeArgsForHandoff,
|
|
38
|
+
} = require('./running-update.cjs');
|
|
29
39
|
const {
|
|
30
40
|
ensurePrivateDirectory,
|
|
31
41
|
securePrivateFile,
|
|
@@ -42,6 +52,7 @@ const RAW_ARGS = process.argv.slice(2);
|
|
|
42
52
|
const PROFILE = parseProfileLaunchArgs(RAW_ARGS, launcherModeFromArgv(process.argv));
|
|
43
53
|
const ARGS = PROFILE.args;
|
|
44
54
|
const CORE_LOAD_TIMEOUT_MS = 30_000;
|
|
55
|
+
const RUNTIME_READY_TIMEOUT_MS = 60_000;
|
|
45
56
|
|
|
46
57
|
function normalizeWindowsPath(value) {
|
|
47
58
|
return path.win32.normalize(value).replace(/\\+$/u, '').toLowerCase();
|
|
@@ -130,10 +141,11 @@ function shouldDetachProtectedCore(options = {}) {
|
|
|
130
141
|
return platform !== 'win32' || stdinIsTTY !== true;
|
|
131
142
|
}
|
|
132
143
|
|
|
133
|
-
function spawnProtectedCore(args, env, cwd) {
|
|
144
|
+
function spawnProtectedCore(args, env, cwd, options = {}) {
|
|
145
|
+
const packageRoot = path.resolve(options.packageRoot || PKG);
|
|
134
146
|
const child = spawn(
|
|
135
147
|
process.execPath,
|
|
136
|
-
[path.join(
|
|
148
|
+
[path.join(packageRoot, 'bin', 'core-bootstrap.js'), ...args],
|
|
137
149
|
{
|
|
138
150
|
cwd,
|
|
139
151
|
detached: shouldDetachProtectedCore(),
|
|
@@ -143,8 +155,10 @@ function spawnProtectedCore(args, env, cwd) {
|
|
|
143
155
|
},
|
|
144
156
|
);
|
|
145
157
|
let loadedSettled = false;
|
|
158
|
+
let readySettled = false;
|
|
146
159
|
let completedSettled = false;
|
|
147
160
|
let resolveLoaded;
|
|
161
|
+
let resolveReady;
|
|
148
162
|
let resolveCompleted;
|
|
149
163
|
const loadTimer = setTimeout(() => {
|
|
150
164
|
try {
|
|
@@ -152,6 +166,7 @@ function spawnProtectedCore(args, env, cwd) {
|
|
|
152
166
|
} catch {}
|
|
153
167
|
}, CORE_LOAD_TIMEOUT_MS);
|
|
154
168
|
const loaded = new Promise((resolve) => { resolveLoaded = resolve; });
|
|
169
|
+
const ready = new Promise((resolve) => { resolveReady = resolve; });
|
|
155
170
|
const completed = new Promise((resolve) => { resolveCompleted = resolve; });
|
|
156
171
|
const settleLoaded = (value) => {
|
|
157
172
|
if (loadedSettled) return;
|
|
@@ -159,11 +174,17 @@ function spawnProtectedCore(args, env, cwd) {
|
|
|
159
174
|
clearTimeout(loadTimer);
|
|
160
175
|
resolveLoaded(value);
|
|
161
176
|
};
|
|
177
|
+
const settleReady = (value) => {
|
|
178
|
+
if (readySettled) return;
|
|
179
|
+
readySettled = true;
|
|
180
|
+
resolveReady(value);
|
|
181
|
+
};
|
|
162
182
|
const settleCompleted = (result) => {
|
|
163
183
|
if (completedSettled) return;
|
|
164
184
|
completedSettled = true;
|
|
165
185
|
for (const [signal, handler] of signalHandlers) process.removeListener(signal, handler);
|
|
166
186
|
settleLoaded(false);
|
|
187
|
+
settleReady(false);
|
|
167
188
|
resolveCompleted(result);
|
|
168
189
|
};
|
|
169
190
|
const signalHandlers = new Map();
|
|
@@ -177,23 +198,93 @@ function spawnProtectedCore(args, env, cwd) {
|
|
|
177
198
|
process.once(signal, handler);
|
|
178
199
|
}
|
|
179
200
|
child.on('message', (message) => {
|
|
180
|
-
if (message?.type
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
child.disconnect();
|
|
184
|
-
} catch {}
|
|
201
|
+
if (message?.type === CORE_LOADED_MESSAGE) settleLoaded(true);
|
|
202
|
+
if (message?.type === RUNTIME_READY_MESSAGE) settleReady(true);
|
|
203
|
+
options.onMessage?.(message, child);
|
|
185
204
|
});
|
|
186
205
|
child.once('error', (error) => settleCompleted({ error }));
|
|
187
206
|
child.once('exit', (code, signal) => settleCompleted({ code, signal }));
|
|
188
|
-
return { child, completed, loaded };
|
|
207
|
+
return { child, completed, loaded, packageRoot, ready };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async function waitForRuntimeReady(core, timeoutMs = RUNTIME_READY_TIMEOUT_MS) {
|
|
211
|
+
let timer;
|
|
212
|
+
const timedOut = new Promise((resolve) => {
|
|
213
|
+
timer = setTimeout(() => resolve(false), timeoutMs);
|
|
214
|
+
timer.unref?.();
|
|
215
|
+
});
|
|
216
|
+
try {
|
|
217
|
+
return await Promise.race([core.ready, timedOut]);
|
|
218
|
+
} finally {
|
|
219
|
+
clearTimeout(timer);
|
|
220
|
+
}
|
|
189
221
|
}
|
|
190
222
|
|
|
191
|
-
async function superviseProtectedCore(args, env, cwd, releaseNotice) {
|
|
192
|
-
const
|
|
223
|
+
async function superviseProtectedCore(args, env, cwd, releaseNotice, options = {}) {
|
|
224
|
+
const packageRoot = path.resolve(options.packageRoot || PKG);
|
|
225
|
+
let preparedTarget;
|
|
226
|
+
let handoffSessionId;
|
|
227
|
+
let updateStarted = false;
|
|
228
|
+
const startPreparation = (child) => {
|
|
229
|
+
if (updateStarted || options.runningUpdate === false) return;
|
|
230
|
+
updateStarted = true;
|
|
231
|
+
const sharedHome = env.BLUN_SHARED_HOME;
|
|
232
|
+
if (typeof sharedHome !== 'string' || sharedHome.length === 0) return;
|
|
233
|
+
Promise.resolve((options.prepareRunningUpdate || prepareRunningUpdate)({
|
|
234
|
+
currentVersion: JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8')).version,
|
|
235
|
+
sharedHome,
|
|
236
|
+
env,
|
|
237
|
+
})).then((target) => {
|
|
238
|
+
if (target === null || child.connected !== true) return;
|
|
239
|
+
preparedTarget = target;
|
|
240
|
+
child.send({ type: RUNNING_UPDATE_PREPARED_MESSAGE, version: target.version });
|
|
241
|
+
}).catch((error) => {
|
|
242
|
+
options.onRunningUpdateError?.(error);
|
|
243
|
+
});
|
|
244
|
+
};
|
|
245
|
+
const core = spawnProtectedCore(args, env, cwd, {
|
|
246
|
+
packageRoot,
|
|
247
|
+
onMessage(message, child) {
|
|
248
|
+
if (message?.type === RUNTIME_READY_MESSAGE) startPreparation(child);
|
|
249
|
+
if (message?.type === RUNNING_UPDATE_HANDOFF_MESSAGE
|
|
250
|
+
&& typeof message.sessionId === 'string'
|
|
251
|
+
&& message.sessionId.length > 0) {
|
|
252
|
+
handoffSessionId = message.sessionId;
|
|
253
|
+
}
|
|
254
|
+
},
|
|
255
|
+
});
|
|
193
256
|
const loaded = await core.loaded;
|
|
194
257
|
await releaseNotice();
|
|
195
258
|
const result = await core.completed;
|
|
196
259
|
if (result.error) throw result.error;
|
|
260
|
+
if (result.code === RUNNING_UPDATE_HANDOFF_EXIT_CODE
|
|
261
|
+
&& preparedTarget !== undefined
|
|
262
|
+
&& handoffSessionId !== undefined) {
|
|
263
|
+
const handoff = await handoffRuntime({
|
|
264
|
+
target: preparedTarget,
|
|
265
|
+
previous: { packageRoot, version: readPackageVersion() },
|
|
266
|
+
sessionId: handoffSessionId,
|
|
267
|
+
args,
|
|
268
|
+
probeTarget: options.probeRunningUpdate || (() => true),
|
|
269
|
+
stopOld: async () => {},
|
|
270
|
+
startCore: async ({ packageRoot: nextRoot, args: nextArgs }) => {
|
|
271
|
+
const nextCore = spawnProtectedCore(nextArgs, env, cwd, { packageRoot: nextRoot });
|
|
272
|
+
const nextLoaded = await nextCore.loaded;
|
|
273
|
+
const ready = nextLoaded && await waitForRuntimeReady(nextCore, options.readyTimeoutMs);
|
|
274
|
+
if (!ready) {
|
|
275
|
+
try { nextCore.child.kill(); } catch {}
|
|
276
|
+
await nextCore.completed;
|
|
277
|
+
}
|
|
278
|
+
return { ...nextCore, ready };
|
|
279
|
+
},
|
|
280
|
+
activateTarget: (target) => (options.activateRuntime || activateRuntime)(env.BLUN_SHARED_HOME, target),
|
|
281
|
+
});
|
|
282
|
+
const resumed = handoff.core;
|
|
283
|
+
if (resumed === undefined || resumed.ready !== true) return 1;
|
|
284
|
+
const resumedResult = await resumed.completed;
|
|
285
|
+
if (resumedResult.error) throw resumedResult.error;
|
|
286
|
+
return exitCodeForChild(resumedResult, true);
|
|
287
|
+
}
|
|
197
288
|
return exitCodeForChild(result, loaded);
|
|
198
289
|
}
|
|
199
290
|
|
|
@@ -246,8 +337,40 @@ function spawnManagedLauncher(binary, cwd) {
|
|
|
246
337
|
});
|
|
247
338
|
}
|
|
248
339
|
|
|
340
|
+
function spawnActiveLauncher(packageRoot, cwd) {
|
|
341
|
+
const entryName = path.basename(process.argv[1] || 'king.js').toLowerCase() === 'blun.js'
|
|
342
|
+
? 'blun.js'
|
|
343
|
+
: 'king.js';
|
|
344
|
+
return new Promise((resolve, reject) => {
|
|
345
|
+
const child = spawn(process.execPath, [path.join(packageRoot, 'bin', entryName), ...RAW_ARGS], {
|
|
346
|
+
cwd,
|
|
347
|
+
env: { ...process.env, BLUN_ACTIVE_RUNTIME_ROOT: packageRoot },
|
|
348
|
+
stdio: 'inherit',
|
|
349
|
+
windowsHide: true,
|
|
350
|
+
});
|
|
351
|
+
child.once('error', reject);
|
|
352
|
+
child.once('exit', (code, signal) => {
|
|
353
|
+
if (Number.isInteger(code)) resolve(code);
|
|
354
|
+
else if (signal === 'SIGINT') resolve(130);
|
|
355
|
+
else if (signal === 'SIGTERM') resolve(143);
|
|
356
|
+
else resolve(1);
|
|
357
|
+
});
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
|
|
249
361
|
async function runLauncher(options = {}) {
|
|
250
362
|
const callerCwd = process.cwd();
|
|
363
|
+
if (options.skipActiveRuntime !== true) {
|
|
364
|
+
const activeHome = resolveLauncherPrivatePaths().sharedHome;
|
|
365
|
+
const activeRuntime = readActiveRuntime(activeHome);
|
|
366
|
+
const currentVersion = readPackageVersion();
|
|
367
|
+
if (activeRuntime !== null
|
|
368
|
+
&& path.resolve(activeRuntime.packageRoot) !== PKG
|
|
369
|
+
&& compareSemver(currentVersion, activeRuntime.version) === -1) {
|
|
370
|
+
process.exitCode = await spawnActiveLauncher(activeRuntime.packageRoot, callerCwd);
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
251
374
|
const mode = options.mode || launcherModeFromArgv(process.argv);
|
|
252
375
|
const explicitUpdateRequest = isNpmPackageUpdateRequest(ARGS);
|
|
253
376
|
const explicitToolsRequest = String(ARGS[0] || '').toLowerCase() === 'tools';
|
package/bin/profile-runtime.cjs
CHANGED
|
@@ -26,6 +26,8 @@ const LEGACY_PROFILE_ENTRIES = Object.freeze([
|
|
|
26
26
|
'tui.toml',
|
|
27
27
|
]);
|
|
28
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';
|
|
29
31
|
|
|
30
32
|
function normalizeProfileName(value) {
|
|
31
33
|
const normalized = String(value || '').trim().toLowerCase();
|
|
@@ -102,27 +104,161 @@ function copyLegacyEntry(source, target, fsImpl) {
|
|
|
102
104
|
if (stat.isFile()) fsImpl.copyFileSync(source, target);
|
|
103
105
|
}
|
|
104
106
|
|
|
105
|
-
function
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
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
|
+
}
|
|
109
114
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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}`);
|
|
113
118
|
}
|
|
119
|
+
const temporary = `${target}.migration-${process.pid}-${Date.now()}.tmp`;
|
|
120
|
+
fsImpl.writeFileSync(temporary, replacement, 'utf8');
|
|
114
121
|
try {
|
|
115
|
-
fsImpl.
|
|
122
|
+
fsImpl.renameSync(temporary, target);
|
|
116
123
|
} catch (error) {
|
|
117
|
-
|
|
124
|
+
try {
|
|
125
|
+
fsImpl.rmSync(temporary, { force: true });
|
|
126
|
+
} catch {}
|
|
127
|
+
throw error;
|
|
118
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');
|
|
119
237
|
return paths;
|
|
120
238
|
}
|
|
121
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
|
+
|
|
122
257
|
module.exports = {
|
|
123
258
|
DEFAULT_PROFILE,
|
|
124
259
|
normalizeProfileName,
|
|
125
260
|
parseProfileLaunchArgs,
|
|
261
|
+
repairDefaultProfileSessionPaths,
|
|
126
262
|
resolveProfilePaths,
|
|
127
263
|
seedDefaultProfileFromLegacy,
|
|
128
264
|
};
|
|
@@ -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
|
+
};
|
package/bin/update-notice.js
CHANGED
|
@@ -54,8 +54,18 @@ const LEGACY_MANIFEST_KEYS = Object.freeze([
|
|
|
54
54
|
'notesUrl',
|
|
55
55
|
'releasedAt',
|
|
56
56
|
]);
|
|
57
|
+
const RELEASE_NOTES_MANIFEST_KEYS = Object.freeze([
|
|
58
|
+
'install',
|
|
59
|
+
'latest',
|
|
60
|
+
'minSupported',
|
|
61
|
+
'name',
|
|
62
|
+
'notesUrl',
|
|
63
|
+
'releaseNotes',
|
|
64
|
+
'releasedAt',
|
|
65
|
+
]);
|
|
57
66
|
const MANIFEST_KEYS = Object.freeze([
|
|
58
67
|
'install',
|
|
68
|
+
'integrity',
|
|
59
69
|
'latest',
|
|
60
70
|
'minSupported',
|
|
61
71
|
'name',
|
|
@@ -214,8 +224,20 @@ function parseReleaseNotes(value) {
|
|
|
214
224
|
return Object.freeze(notes);
|
|
215
225
|
}
|
|
216
226
|
|
|
227
|
+
function validIntegrity(value) {
|
|
228
|
+
if (typeof value !== 'string') return false;
|
|
229
|
+
const match = /^sha512-([A-Za-z0-9+/]+={0,2})$/u.exec(value);
|
|
230
|
+
if (!match) return false;
|
|
231
|
+
try {
|
|
232
|
+
return Buffer.from(match[1], 'base64').length === 64;
|
|
233
|
+
} catch {
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
217
238
|
function parseFallbackManifest(value) {
|
|
218
239
|
const record = exactDataRecord(value, MANIFEST_KEYS)
|
|
240
|
+
|| exactDataRecord(value, RELEASE_NOTES_MANIFEST_KEYS)
|
|
219
241
|
|| exactDataRecord(value, LEGACY_MANIFEST_KEYS);
|
|
220
242
|
const notesUrl = record ? canonicalOfficialNotesUrl(record.notesUrl) : undefined;
|
|
221
243
|
const releaseNotes = record && Object.hasOwn(record, 'releaseNotes')
|
|
@@ -228,6 +250,7 @@ function parseFallbackManifest(value) {
|
|
|
228
250
|
|| compareSemver(record.minSupported, record.latest) === 1
|
|
229
251
|
|| !isIsoTimestamp(record.releasedAt)
|
|
230
252
|
|| !notesUrl
|
|
253
|
+
|| (Object.hasOwn(record, 'integrity') && !validIntegrity(record.integrity))
|
|
231
254
|
|| (Object.hasOwn(record, 'releaseNotes') && releaseNotes === undefined)
|
|
232
255
|
|| !isBoundedText(record.install, 256)) {
|
|
233
256
|
return undefined;
|
|
@@ -238,6 +261,7 @@ function parseFallbackManifest(value) {
|
|
|
238
261
|
minSupported: record.minSupported,
|
|
239
262
|
releasedAt: record.releasedAt,
|
|
240
263
|
notesUrl,
|
|
264
|
+
...(Object.hasOwn(record, 'integrity') ? { integrity: record.integrity } : {}),
|
|
241
265
|
...(releaseNotes === undefined ? {} : { releaseNotes }),
|
|
242
266
|
install: record.install,
|
|
243
267
|
});
|
package/blun.mjs
CHANGED
|
@@ -503099,8 +503099,8 @@ registerUiCatalogFragment({
|
|
|
503099
503099
|
* 0 = most recent clipped lines visible
|
|
503100
503100
|
* N = N lines up from the bottom
|
|
503101
503101
|
*/
|
|
503102
|
-
/**
|
|
503103
|
-
const MAX_BUFFER_LINES =
|
|
503102
|
+
/** Retain the complete rendered session unless the process itself ends. */
|
|
503103
|
+
const MAX_BUFFER_LINES = Number.POSITIVE_INFINITY;
|
|
503104
503104
|
var ScrollbackBuffer = class {
|
|
503105
503105
|
lines = [];
|
|
503106
503106
|
scrollOffset = 0;
|
|
@@ -505264,6 +505264,7 @@ var SessionEventHandler = class {
|
|
|
505264
505264
|
this.goalCompletionTurnEnded = true;
|
|
505265
505265
|
this.scheduleQueuedGoalPromotion();
|
|
505266
505266
|
if (event.reason === "completed") this.host.refreshManagedQuotaWindows();
|
|
505267
|
+
requestRunningUpdateAtSafeBoundary(this.host);
|
|
505267
505268
|
}
|
|
505268
505269
|
handleStepBegin(event) {
|
|
505269
505270
|
this.host.commitQueuedSteerAtStepStart(event.turnId);
|
|
@@ -505311,12 +505312,13 @@ var SessionEventHandler = class {
|
|
|
505311
505312
|
const duration = durationMs === void 0 ? "—" : formatResponseDuration(durationMs);
|
|
505312
505313
|
const tokens = this.currentTurnTokenCount === void 0 ? "—" : formatTokenCount(this.currentTurnTokenCount);
|
|
505313
505314
|
const tokenKey = this.currentTurnTokenCount === 1 ? "thinking.tokens.one" : "thinking.tokens.other";
|
|
505315
|
+
const startedAt = formatLiveStartedAt(this.currentTurnStartedAtMs ?? Date.now());
|
|
505314
505316
|
this.host.appendTranscriptEntry({
|
|
505315
505317
|
id: nextTranscriptId(),
|
|
505316
505318
|
kind: "status",
|
|
505317
505319
|
turnId: String(event.turnId),
|
|
505318
505320
|
renderMode: "plain",
|
|
505319
|
-
content: `${duration} · ${uiText(tokenKey, { count: tokens })}`
|
|
505321
|
+
content: `${duration} · ${uiText(tokenKey, { count: tokens })} · ${startedAt}`
|
|
505320
505322
|
});
|
|
505321
505323
|
}
|
|
505322
505324
|
markActiveAgentSwarmsCancelled() {
|
|
@@ -506297,12 +506299,6 @@ function createReplayRenderContext() {
|
|
|
506297
506299
|
suppressNextPlanModeOffNotice: false
|
|
506298
506300
|
};
|
|
506299
506301
|
}
|
|
506300
|
-
function limitReplayRecordsByTurn(records, maxTurns) {
|
|
506301
|
-
if (maxTurns <= 0) return [];
|
|
506302
|
-
const turnStarts = records.flatMap((record, index) => isReplayUserTurnRecord(record) ? [index] : []);
|
|
506303
|
-
if (turnStarts.length <= maxTurns) return records;
|
|
506304
|
-
return records.slice(turnStarts[turnStarts.length - maxTurns]);
|
|
506305
|
-
}
|
|
506306
506302
|
function replayEntry(context, kind, content, renderMode, extras = {}) {
|
|
506307
506303
|
return {
|
|
506308
506304
|
id: nextTranscriptId(),
|
|
@@ -506630,7 +506626,7 @@ var SessionReplayRenderer = class {
|
|
|
506630
506626
|
}
|
|
506631
506627
|
renderRecords(agent) {
|
|
506632
506628
|
const context = createReplayRenderContext();
|
|
506633
|
-
for (const record of
|
|
506629
|
+
for (const record of agent.replay) this.renderRecord(context, record);
|
|
506634
506630
|
this.flushAssistant(context);
|
|
506635
506631
|
this.cleanupRuntime(context);
|
|
506636
506632
|
}
|
|
@@ -508034,7 +508030,7 @@ var FooterComponent = class {
|
|
|
508034
508030
|
const persona = personaName();
|
|
508035
508031
|
const profileLabel = process.env["BLUN_PROFILE"] ?? "default";
|
|
508036
508032
|
const profile = chalk.hex(colors.textDim)(profileLabel) + sep;
|
|
508037
|
-
const brand = chalk.hex(colors.primary).bold("●
|
|
508033
|
+
const brand = chalk.hex(colors.primary).bold("● BL") + chalk.hex(colors.text)("UN") + sep + profile + (persona !== void 0 ? chalk.hex(colors.text)(persona) + sep : "");
|
|
508038
508034
|
const modelLabel = model;
|
|
508039
508035
|
const modeLabel = uiText(state.permissionMode === "yolo" ? "footer.mode.god" : state.permissionMode === "auto" ? "footer.mode.auto" : "footer.mode.manual");
|
|
508040
508036
|
const modeSuffix = sep + chalk.hex(colors.textDim)(uiText("footer.mode", { mode: modeLabel }));
|
|
@@ -511396,12 +511392,18 @@ var CustomEditor = class extends Editor {
|
|
|
511396
511392
|
if (isKeyRelease(normalized)) return;
|
|
511397
511393
|
if (!matchesKey(normalized, Key.escape)) this.onNonEscapeInput?.();
|
|
511398
511394
|
if (this.consumingPaste) {
|
|
511399
|
-
|
|
511400
|
-
if (
|
|
511395
|
+
const cancelPaste = matchesKey(normalized, Key.ctrl("c")) || matchesKey(normalized, Key.escape);
|
|
511396
|
+
if (cancelPaste) {
|
|
511401
511397
|
this.consumingPaste = false;
|
|
511402
511398
|
this.consumeBuffer = "";
|
|
511399
|
+
} else {
|
|
511400
|
+
this.consumeBuffer += normalized;
|
|
511401
|
+
if (this.consumeBuffer.includes(BRACKET_PASTE_END)) {
|
|
511402
|
+
this.consumingPaste = false;
|
|
511403
|
+
this.consumeBuffer = "";
|
|
511404
|
+
}
|
|
511405
|
+
return;
|
|
511403
511406
|
}
|
|
511404
|
-
return;
|
|
511405
511407
|
}
|
|
511406
511408
|
if (normalized.includes(BRACKET_PASTE_START) && this.expandPasteMarkerAtCursor()) {
|
|
511407
511409
|
if (!normalized.includes(BRACKET_PASTE_END)) this.consumingPaste = true;
|
|
@@ -511456,6 +511458,7 @@ var CustomEditor = class extends Editor {
|
|
|
511456
511458
|
if (this.inputMode === "bash" && this.getText().length === 0 && (matchesKey(normalized, Key.escape) || matchesKey(normalized, Key.backspace))) {
|
|
511457
511459
|
this.inputMode = "prompt";
|
|
511458
511460
|
this.onInputModeChange?.("prompt");
|
|
511461
|
+
if (matchesKey(normalized, Key.escape)) this.onEscape?.(true);
|
|
511459
511462
|
return;
|
|
511460
511463
|
}
|
|
511461
511464
|
if (matchesKey(normalized, Key.up)) {
|
|
@@ -511742,6 +511745,7 @@ var GhostSuggestEditor = class extends CustomEditor {
|
|
|
511742
511745
|
if (matchesKey(normalized, Key.escape)) {
|
|
511743
511746
|
this.ghostSuggestion = null;
|
|
511744
511747
|
this.onGhostDismissed?.();
|
|
511748
|
+
this.onEscape?.(true);
|
|
511745
511749
|
return;
|
|
511746
511750
|
}
|
|
511747
511751
|
if (this.isCursorAtTextEnd() && !this.isShowingAutocomplete()) {
|
|
@@ -513049,13 +513053,13 @@ function readEnvInt(name, fallback) {
|
|
|
513049
513053
|
return value;
|
|
513050
513054
|
}
|
|
513051
513055
|
/** Keep the most recent N turns. `0` disables trimming. */
|
|
513052
|
-
const TRANSCRIPT_MAX_TURNS = readEnvInt("BLUN_TUI_MAX_TURNS",
|
|
513056
|
+
const TRANSCRIPT_MAX_TURNS = readEnvInt("BLUN_TUI_MAX_TURNS", 0);
|
|
513053
513057
|
/** Only the most recent E turns are allowed to expand (Ctrl+O). `0` disables expanding. */
|
|
513054
513058
|
const TRANSCRIPT_EXPAND_TURNS = readEnvInt("BLUN_TUI_EXPAND_TURNS", 3);
|
|
513055
513059
|
/** Only trim once the window exceeds maxTurns by this much (avoids churn). */
|
|
513056
513060
|
const TRANSCRIPT_HYSTERESIS = readEnvInt("BLUN_TUI_HYSTERESIS", 5);
|
|
513057
513061
|
/** Keep this many recent steps untouched inside a turn; older steps are merged into a summary. `0` disables merging. */
|
|
513058
|
-
const TRANSCRIPT_KEEP_RECENT_STEPS = readEnvInt("BLUN_TUI_KEEP_RECENT_STEPS",
|
|
513062
|
+
const TRANSCRIPT_KEEP_RECENT_STEPS = readEnvInt("BLUN_TUI_KEEP_RECENT_STEPS", 0);
|
|
513059
513063
|
/**
|
|
513060
513064
|
* Group consecutive entries into turns by `turnId`. Entries with the same
|
|
513061
513065
|
* non-undefined `turnId` that are adjacent belong to the same turn.
|
|
@@ -513112,6 +513116,60 @@ function turnsToTrim(turns, maxTurns, hysteresis) {
|
|
|
513112
513116
|
}
|
|
513113
513117
|
//#endregion
|
|
513114
513118
|
//#region src/tui/blun-tui.ts
|
|
513119
|
+
const {
|
|
513120
|
+
RUNNING_UPDATE_HANDOFF_EXIT_CODE,
|
|
513121
|
+
RUNNING_UPDATE_HANDOFF_MESSAGE,
|
|
513122
|
+
RUNNING_UPDATE_PREPARED_MESSAGE,
|
|
513123
|
+
RUNTIME_READY_MESSAGE,
|
|
513124
|
+
isSafeRuntimeBoundary
|
|
513125
|
+
} = __require("./bin/running-update.cjs");
|
|
513126
|
+
function notifyRunningRuntimeReady(tui) {
|
|
513127
|
+
if (!process.connected) return;
|
|
513128
|
+
const sessionId = tui.getCurrentSessionId();
|
|
513129
|
+
if (sessionId.length === 0) return;
|
|
513130
|
+
try {
|
|
513131
|
+
process.send({
|
|
513132
|
+
type: RUNTIME_READY_MESSAGE,
|
|
513133
|
+
sessionId
|
|
513134
|
+
});
|
|
513135
|
+
} catch {}
|
|
513136
|
+
}
|
|
513137
|
+
function requestRunningUpdateAtSafeBoundary(tui) {
|
|
513138
|
+
if (tui.runningUpdatePreparedVersion === void 0 || tui.runningUpdateHandoffStarted || !process.connected) return false;
|
|
513139
|
+
if (!isSafeRuntimeBoundary({
|
|
513140
|
+
isShuttingDown: tui.isShuttingDown,
|
|
513141
|
+
streamingPhase: tui.state.appState.streamingPhase,
|
|
513142
|
+
isCompacting: tui.state.appState.isCompacting,
|
|
513143
|
+
queuedMessages: tui.state.queuedMessages.length,
|
|
513144
|
+
activeToolCalls: tui.streamingUI.hasActiveToolCalls() ? 1 : 0,
|
|
513145
|
+
shellCommands: tui.shellOutputStreams.size,
|
|
513146
|
+
queueCommandRunning: tui.queueCommandRunning
|
|
513147
|
+
})) return false;
|
|
513148
|
+
const sessionId = tui.getCurrentSessionId();
|
|
513149
|
+
if (sessionId.length === 0) return false;
|
|
513150
|
+
tui.runningUpdateHandoffStarted = true;
|
|
513151
|
+
try {
|
|
513152
|
+
process.send({
|
|
513153
|
+
type: RUNNING_UPDATE_HANDOFF_MESSAGE,
|
|
513154
|
+
sessionId,
|
|
513155
|
+
version: tui.runningUpdatePreparedVersion
|
|
513156
|
+
});
|
|
513157
|
+
} catch {
|
|
513158
|
+
tui.runningUpdateHandoffStarted = false;
|
|
513159
|
+
return false;
|
|
513160
|
+
}
|
|
513161
|
+
void tui.stop(RUNNING_UPDATE_HANDOFF_EXIT_CODE);
|
|
513162
|
+
return true;
|
|
513163
|
+
}
|
|
513164
|
+
function installRunningUpdateListener(tui) {
|
|
513165
|
+
const handler = (message) => {
|
|
513166
|
+
if (message?.type !== RUNNING_UPDATE_PREPARED_MESSAGE || typeof message.version !== "string") return;
|
|
513167
|
+
tui.runningUpdatePreparedVersion = message.version;
|
|
513168
|
+
requestRunningUpdateAtSafeBoundary(tui);
|
|
513169
|
+
};
|
|
513170
|
+
process.on("message", handler);
|
|
513171
|
+
return () => process.off("message", handler);
|
|
513172
|
+
}
|
|
513115
513173
|
function loadingTipKind(mode) {
|
|
513116
513174
|
if (mode === "waiting" || mode === "tool") return "blun";
|
|
513117
513175
|
if (mode === "composing") return "composing";
|
|
@@ -513196,6 +513254,9 @@ var BlunTUI = class {
|
|
|
513196
513254
|
startupLoginRequired = false;
|
|
513197
513255
|
startupWorkspaceSelectionPending = false;
|
|
513198
513256
|
startupGoalPromptedSessionId;
|
|
513257
|
+
runningUpdatePreparedVersion;
|
|
513258
|
+
runningUpdateHandoffStarted = false;
|
|
513259
|
+
runningUpdateMessageDispose;
|
|
513199
513260
|
startupPhaseMs = {};
|
|
513200
513261
|
lastActivityMode;
|
|
513201
513262
|
currentLoadingTip = void 0;
|
|
@@ -513309,6 +513370,7 @@ var BlunTUI = class {
|
|
|
513309
513370
|
this.editorKeyboard.install();
|
|
513310
513371
|
this.scrollbackController = new ScrollbackController(this.state);
|
|
513311
513372
|
this.scrollbackController.install();
|
|
513373
|
+
this.runningUpdateMessageDispose = installRunningUpdateListener(this);
|
|
513312
513374
|
this.buildLayout();
|
|
513313
513375
|
}
|
|
513314
513376
|
getSlashCommands() {
|
|
@@ -513739,6 +513801,8 @@ var BlunTUI = class {
|
|
|
513739
513801
|
this.managedQuotaWarningController.dispose();
|
|
513740
513802
|
this.state.loopIndicator.dispose();
|
|
513741
513803
|
this.state.footer.dispose();
|
|
513804
|
+
this.runningUpdateMessageDispose?.();
|
|
513805
|
+
this.runningUpdateMessageDispose = void 0;
|
|
513742
513806
|
for (const dispose of this.reverseRpcDisposers) dispose();
|
|
513743
513807
|
this.reverseRpcDisposers.length = 0;
|
|
513744
513808
|
this.disposeTerminalTracking();
|
|
@@ -513962,6 +514026,7 @@ var BlunTUI = class {
|
|
|
513962
514026
|
if (this.shellOutputStreams.size === 0) {
|
|
513963
514027
|
this.setAppState({ streamingPhase: "idle" });
|
|
513964
514028
|
this.drainOneQueuedMessage();
|
|
514029
|
+
requestRunningUpdateAtSafeBoundary(this);
|
|
513965
514030
|
}
|
|
513966
514031
|
}
|
|
513967
514032
|
queueDrainTimer;
|
|
@@ -516409,22 +516474,27 @@ async function runShell(opts, version, updateStartupNotice) {
|
|
|
516409
516474
|
tui.onExit = async (exitCode = 0) => {
|
|
516410
516475
|
const sessionId = tui.getCurrentSessionId();
|
|
516411
516476
|
const hasContent = tui.hasSessionContent();
|
|
516477
|
+
const runningUpdateHandoff = exitCode === RUNNING_UPDATE_HANDOFF_EXIT_CODE;
|
|
516412
516478
|
setCrashPhase("shutdown");
|
|
516413
516479
|
trackLifecycle("exit", { duration_ms: Date.now() - startedAt });
|
|
516414
516480
|
await shutdownTelemetry({ timeoutMs: CLI_SHUTDOWN_TIMEOUT_MS });
|
|
516415
516481
|
const gutter = " ".repeat(1);
|
|
516416
|
-
process.stdout.write(`${gutter}Bye!\n`);
|
|
516482
|
+
if (!runningUpdateHandoff) process.stdout.write(`${gutter}Bye!\n`);
|
|
516417
516483
|
const hints = [];
|
|
516418
|
-
if (sessionId !== "" && hasContent) hints.push(`${gutter}To resume this session: blun -r ${sessionId}`);
|
|
516419
|
-
if (tui.exitOpenUrl !== void 0) hints.push(`${gutter}open ${toTerminalHyperlink$1(tui.exitOpenUrl, tui.exitOpenUrl)}`);
|
|
516484
|
+
if (!runningUpdateHandoff && sessionId !== "" && hasContent) hints.push(`${gutter}To resume this session: blun -r ${sessionId}`);
|
|
516485
|
+
if (!runningUpdateHandoff && tui.exitOpenUrl !== void 0) hints.push(`${gutter}open ${toTerminalHyperlink$1(tui.exitOpenUrl, tui.exitOpenUrl)}`);
|
|
516420
516486
|
if (hints.length > 0) process.stderr.write(`\n${hints.join("\n")}\n`);
|
|
516421
516487
|
removeCrashHandlers();
|
|
516422
516488
|
restoreStty();
|
|
516489
|
+
if (process.connected) try {
|
|
516490
|
+
process.disconnect();
|
|
516491
|
+
} catch {}
|
|
516423
516492
|
process.exit(exitCode);
|
|
516424
516493
|
};
|
|
516425
516494
|
try {
|
|
516426
516495
|
const initStartedAt = Date.now();
|
|
516427
516496
|
await tui.start();
|
|
516497
|
+
notifyRunningRuntimeReady(tui);
|
|
516428
516498
|
const initMs = Date.now() - initStartedAt;
|
|
516429
516499
|
const startupSessionId = tui.getCurrentSessionId();
|
|
516430
516500
|
const mcpMs = await tui.getStartupMcpMs();
|