@caelis/caelis 0.28.0 → 0.30.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/bin/caelis.js CHANGED
@@ -4,6 +4,13 @@ const { spawn } = require('node:child_process');
4
4
  const fs = require('node:fs');
5
5
  const path = require('node:path');
6
6
 
7
+ const {
8
+ UpdateHandoffError,
9
+ completeHandoff,
10
+ handoffEnvironment,
11
+ reserveHandoffDirectory,
12
+ } = require('../lib/update-handoff.js');
13
+
7
14
  const packageMap = {
8
15
  'darwin:arm64': '@caelis/caelis-darwin-arm64',
9
16
  'darwin:x64': '@caelis/caelis-darwin-x64',
@@ -13,60 +20,287 @@ const packageMap = {
13
20
  'win32:x64': '@caelis/caelis-windows-x64',
14
21
  };
15
22
 
16
- function resolvePackageName() {
17
- const key = `${process.platform}:${process.arch}`;
23
+ const signalForwardGraceMs = 5000;
24
+
25
+ class LauncherError extends Error {
26
+ constructor(lines) {
27
+ const normalized = Array.isArray(lines) ? lines : [String(lines || '')];
28
+ super(normalized.join('\n'));
29
+ this.name = 'LauncherError';
30
+ this.lines = normalized;
31
+ }
32
+ }
33
+
34
+ function resolvePackageName(platform = process.platform, arch = process.arch) {
35
+ const key = `${platform}:${arch}`;
18
36
  const packageName = packageMap[key];
19
37
  if (!packageName) {
20
- console.error(`[caelis] unsupported platform/arch: ${process.platform}/${process.arch}`);
21
- process.exit(1);
38
+ throw new Error(`unsupported platform/arch: ${platform}/${arch}`);
22
39
  }
23
40
  return packageName;
24
41
  }
25
42
 
26
- function resolveBinaryPath(packageName) {
43
+ function resolveBinaryPath(
44
+ packageName,
45
+ platform = process.platform,
46
+ resolver = require.resolve,
47
+ ) {
48
+ let packageJsonPath;
27
49
  try {
28
- const packageJsonPath = require.resolve(`${packageName}/package.json`);
29
- const binaryName = process.platform === 'win32' ? 'caelis.exe' : 'caelis';
30
- return path.join(path.dirname(packageJsonPath), 'runtime', binaryName);
50
+ packageJsonPath = resolver(`${packageName}/package.json`);
31
51
  } catch (err) {
32
- console.error(`[caelis] platform package not installed: ${packageName}`);
33
- console.error('[caelis] reinstall without --omit=optional, then try again.');
34
- console.error('[caelis] resolve error:', err.message);
35
- process.exit(1);
52
+ throw new LauncherError([
53
+ `platform package not installed: ${packageName}`,
54
+ 'reinstall without --omit=optional, then try again',
55
+ `resolve error: ${err.message}`,
56
+ ]);
57
+ }
58
+ const binaryName = platform === 'win32' ? 'caelis.exe' : 'caelis';
59
+ return path.join(path.dirname(packageJsonPath), 'runtime', binaryName);
60
+ }
61
+
62
+ function handoffEligible(platform, argv, stdinIsTTY) {
63
+ if (platform !== 'win32') {
64
+ return false;
65
+ }
66
+ const first = String(argv[0] || '').trim().toLowerCase();
67
+ if (first === 'update') {
68
+ return !argv.some((value) => value === '--check' || value === '-check');
36
69
  }
70
+ const nonInteractiveCommands = new Set([
71
+ 'version',
72
+ 'acp',
73
+ 'doctor',
74
+ 'serve',
75
+ 'server',
76
+ 'sandbox',
77
+ ]);
78
+ if (nonInteractiveCommands.has(first) ||
79
+ argv.some((value) => value === '--doctor' || value === '-doctor')) {
80
+ return false;
81
+ }
82
+ if (argv.some((value) =>
83
+ value === '--interactive' || value === '-interactive')) {
84
+ return true;
85
+ }
86
+ const hasPrompt = argv.some((value, index) => {
87
+ if (value === '-p') {
88
+ return index + 1 < argv.length && String(argv[index + 1]).trim() !== '';
89
+ }
90
+ return String(value).startsWith('-p=') &&
91
+ String(value).slice(3).trim() !== '';
92
+ });
93
+ if (hasPrompt) {
94
+ return false;
95
+ }
96
+ return Boolean(stdinIsTTY);
37
97
  }
38
98
 
39
- const packageName = resolvePackageName();
40
- const binPath = resolveBinaryPath(packageName);
41
- const packageRoot = path.resolve(__dirname, '..');
42
- const platformPackageRoot = path.dirname(require.resolve(`${packageName}/package.json`));
99
+ function formatLauncherError(err) {
100
+ if (err instanceof UpdateHandoffError) {
101
+ return `[caelis] update failed: ${err.message}`;
102
+ }
103
+ if (err instanceof LauncherError) {
104
+ return err.lines.map((line) => `[caelis] ${line}`).join('\n');
105
+ }
106
+ return `[caelis] ${err && err.message ? err.message : String(err)}`;
107
+ }
108
+
109
+ function createSignalCoordinator(target = process, options = {}) {
110
+ const forceKillAfterMs = options.forceKillAfterMs === undefined
111
+ ? signalForwardGraceMs
112
+ : options.forceKillAfterMs;
113
+ const handlers = new Map();
114
+ let activeChild;
115
+ let forceKillTimer;
116
+ let signal;
117
+
118
+ function clearForceKillTimer() {
119
+ if (forceKillTimer) {
120
+ clearTimeout(forceKillTimer);
121
+ forceKillTimer = undefined;
122
+ }
123
+ }
124
+
125
+ function forwardSignal() {
126
+ if (!signal || !activeChild) {
127
+ return;
128
+ }
129
+ try {
130
+ activeChild.kill(signal);
131
+ } catch {
132
+ // Still arm the hard-stop fallback below.
133
+ }
134
+ clearForceKillTimer();
135
+ if (forceKillAfterMs < 0) {
136
+ return;
137
+ }
138
+ forceKillTimer = setTimeout(() => {
139
+ if (!activeChild) {
140
+ return;
141
+ }
142
+ try {
143
+ activeChild.kill('SIGKILL');
144
+ } catch {
145
+ // The child may have exited between the timer check and kill.
146
+ }
147
+ }, forceKillAfterMs);
148
+ if (typeof forceKillTimer.unref === 'function') {
149
+ forceKillTimer.unref();
150
+ }
151
+ }
43
152
 
44
- if (!fs.existsSync(binPath)) {
45
- console.error('[caelis] binary not found at', binPath);
46
- console.error(`[caelis] reinstall ${packageName}, then try again.`);
47
- process.exit(1);
153
+ for (const name of ['SIGINT', 'SIGTERM']) {
154
+ const handler = () => {
155
+ if (!signal) {
156
+ signal = name;
157
+ }
158
+ forwardSignal();
159
+ };
160
+ handlers.set(name, handler);
161
+ target.on(name, handler);
162
+ }
163
+
164
+ return {
165
+ receivedSignal() {
166
+ return signal;
167
+ },
168
+ track(child) {
169
+ activeChild = child;
170
+ forwardSignal();
171
+ let released = false;
172
+ return () => {
173
+ if (released) {
174
+ return;
175
+ }
176
+ released = true;
177
+ if (activeChild === child) {
178
+ activeChild = undefined;
179
+ clearForceKillTimer();
180
+ }
181
+ };
182
+ },
183
+ close() {
184
+ clearForceKillTimer();
185
+ activeChild = undefined;
186
+ for (const [name, handler] of handlers) {
187
+ target.removeListener(name, handler);
188
+ }
189
+ handlers.clear();
190
+ },
191
+ };
48
192
  }
49
193
 
50
- const child = spawn(binPath, process.argv.slice(2), {
51
- stdio: 'inherit',
52
- env: {
194
+ function runProcess(command, args, options = {}) {
195
+ const { signalCoordinator, ...spawnOptions } = options;
196
+ const pendingSignal = signalCoordinator && signalCoordinator.receivedSignal();
197
+ if (pendingSignal) {
198
+ return Promise.resolve({ code: 1, signal: pendingSignal });
199
+ }
200
+ return new Promise((resolve, reject) => {
201
+ const child = spawn(command, args, spawnOptions);
202
+ let releaseChild = () => {};
203
+ child.once('error', (err) => {
204
+ releaseChild();
205
+ reject(err);
206
+ });
207
+ child.once('exit', (code, signal) => {
208
+ releaseChild();
209
+ resolve({ code: code === null ? 1 : code, signal });
210
+ });
211
+ if (signalCoordinator) {
212
+ releaseChild = signalCoordinator.track(child);
213
+ }
214
+ });
215
+ }
216
+
217
+ async function main() {
218
+ const platform = process.platform;
219
+ const packageName = resolvePackageName(platform, process.arch);
220
+ const binPath = resolveBinaryPath(packageName, platform);
221
+ const packageRoot = path.resolve(__dirname, '..');
222
+ const platformPackageRoot = path.dirname(path.dirname(binPath));
223
+
224
+ if (!fs.existsSync(binPath)) {
225
+ throw new LauncherError([
226
+ `binary not found at ${binPath}`,
227
+ `reinstall ${packageName}, then try again`,
228
+ ]);
229
+ }
230
+
231
+ const argv = process.argv.slice(2);
232
+ // The TUI can request an update after startup, so eligibility cannot be
233
+ // based only on an explicit `update` argv. Reserving a path does no
234
+ // filesystem I/O; Go creates it lazily only for an actual handoff.
235
+ const handoffDir = handoffEligible(platform, argv, process.stdin.isTTY)
236
+ ? reserveHandoffDirectory(platform)
237
+ : '';
238
+ const signalCoordinator = handoffDir
239
+ ? createSignalCoordinator()
240
+ : undefined;
241
+ const env = {
53
242
  ...process.env,
54
243
  CAELIS_INSTALL_METHOD: 'npm',
55
244
  CAELIS_NPM_PACKAGE_DIR: packageRoot,
56
245
  CAELIS_NPM_PLATFORM_PACKAGE: packageName,
57
246
  CAELIS_NPM_PLATFORM_PACKAGE_DIR: platformPackageRoot,
58
- },
59
- });
247
+ };
248
+ if (handoffDir) {
249
+ env[handoffEnvironment] = handoffDir;
250
+ }
251
+ try {
252
+ let childResult = await runProcess(binPath, argv, {
253
+ stdio: 'inherit',
254
+ env,
255
+ signalCoordinator,
256
+ });
257
+ const receivedSignal = signalCoordinator && signalCoordinator.receivedSignal();
258
+ if (receivedSignal && !childResult.signal) {
259
+ childResult = { code: 1, signal: receivedSignal };
260
+ }
261
+ const result = await completeHandoff(handoffDir, childResult, {
262
+ signalCoordinator,
263
+ });
264
+ const finalSignal = signalCoordinator && signalCoordinator.receivedSignal();
265
+ if (finalSignal) {
266
+ return { code: 1, signal: finalSignal };
267
+ }
268
+ return result;
269
+ } catch (err) {
270
+ const receivedSignal = signalCoordinator && signalCoordinator.receivedSignal();
271
+ if (receivedSignal) {
272
+ return { code: 1, signal: receivedSignal };
273
+ }
274
+ throw err;
275
+ } finally {
276
+ if (signalCoordinator) {
277
+ signalCoordinator.close();
278
+ }
279
+ }
280
+ }
60
281
 
61
- child.on('error', (err) => {
62
- console.error('[caelis] failed to start binary:', err.message);
63
- process.exit(1);
64
- });
282
+ if (require.main === module) {
283
+ main()
284
+ .then((result) => {
285
+ if (result.signal) {
286
+ process.kill(process.pid, result.signal);
287
+ return;
288
+ }
289
+ process.exit(result.code);
290
+ })
291
+ .catch((err) => {
292
+ console.error(formatLauncherError(err));
293
+ process.exit(1);
294
+ });
295
+ }
65
296
 
66
- child.on('exit', (code, signal) => {
67
- if (signal) {
68
- process.kill(process.pid, signal);
69
- return;
70
- }
71
- process.exit(code === null ? 1 : code);
72
- });
297
+ module.exports = {
298
+ LauncherError,
299
+ createSignalCoordinator,
300
+ formatLauncherError,
301
+ handoffEligible,
302
+ main,
303
+ resolveBinaryPath,
304
+ resolvePackageName,
305
+ runProcess,
306
+ };
@@ -0,0 +1,351 @@
1
+ const { spawn } = require('node:child_process');
2
+ const crypto = require('node:crypto');
3
+ const fs = require('node:fs');
4
+ const os = require('node:os');
5
+ const path = require('node:path');
6
+
7
+ const handoffEnvironment = 'CAELIS_NPM_UPDATE_HANDOFF_DIR';
8
+ const handoffOwnershipName = 'ownership.json';
9
+ const handoffPlanName = 'plan.json';
10
+ const capturedOutputLimit = 64 * 1024;
11
+
12
+ class UpdateHandoffError extends Error {
13
+ constructor(message, options = {}) {
14
+ super(message, options);
15
+ this.name = 'UpdateHandoffError';
16
+ }
17
+ }
18
+
19
+ function appendCaptured(previous, chunk) {
20
+ const next = previous + chunk.toString();
21
+ if (next.length <= capturedOutputLimit) {
22
+ return next;
23
+ }
24
+ return next.slice(next.length - capturedOutputLimit);
25
+ }
26
+
27
+ function runCapturedProcess(command, args, options = {}, signalCoordinator) {
28
+ const pendingSignal = signalCoordinator && signalCoordinator.receivedSignal();
29
+ if (pendingSignal) {
30
+ return Promise.resolve({
31
+ code: 1,
32
+ signal: pendingSignal,
33
+ stdout: '',
34
+ stderr: '',
35
+ });
36
+ }
37
+ return new Promise((resolve, reject) => {
38
+ const child = spawn(command, args, {
39
+ ...options,
40
+ stdio: ['inherit', 'pipe', 'pipe'],
41
+ });
42
+ let releaseChild = () => {};
43
+ let stdout = '';
44
+ let stderr = '';
45
+ child.stdout.on('data', (chunk) => {
46
+ stdout = appendCaptured(stdout, chunk);
47
+ });
48
+ child.stderr.on('data', (chunk) => {
49
+ stderr = appendCaptured(stderr, chunk);
50
+ });
51
+ child.once('error', (err) => {
52
+ releaseChild();
53
+ reject(err);
54
+ });
55
+ child.once('exit', (code, signal) => {
56
+ releaseChild();
57
+ resolve({ code: code === null ? 1 : code, signal, stdout, stderr });
58
+ });
59
+ if (signalCoordinator) {
60
+ releaseChild = signalCoordinator.track(child);
61
+ }
62
+ });
63
+ }
64
+
65
+ function createStatusRenderer(stream) {
66
+ const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
67
+ const interactive = Boolean(stream && stream.isTTY);
68
+ let timer;
69
+ let frame = 0;
70
+ let lineWidth = 0;
71
+
72
+ function replaceLine(text, done) {
73
+ if (!stream) {
74
+ return;
75
+ }
76
+ if (!interactive) {
77
+ stream.write(`${text}\n`);
78
+ return;
79
+ }
80
+ const padding = ' '.repeat(Math.max(lineWidth - [...text].length, 0));
81
+ stream.write(`\r${text}${padding}${done ? '\n' : ''}`);
82
+ lineWidth = done ? 0 : [...text].length;
83
+ }
84
+
85
+ function stopTimer() {
86
+ if (timer) {
87
+ clearInterval(timer);
88
+ timer = undefined;
89
+ }
90
+ }
91
+
92
+ return {
93
+ start(label) {
94
+ stopTimer();
95
+ if (!interactive) {
96
+ replaceLine(label, true);
97
+ return;
98
+ }
99
+ replaceLine(`${frames[frame]} ${label}`, false);
100
+ timer = setInterval(() => {
101
+ frame = (frame + 1) % frames.length;
102
+ replaceLine(`${frames[frame]} ${label}`, false);
103
+ }, 80);
104
+ },
105
+ succeed(message) {
106
+ stopTimer();
107
+ replaceLine(`✓ ${message}`, true);
108
+ },
109
+ fail(message) {
110
+ stopTimer();
111
+ replaceLine(`✗ ${message}`, true);
112
+ },
113
+ stop() {
114
+ stopTimer();
115
+ },
116
+ };
117
+ }
118
+
119
+ function validateHandoffPlan(plan) {
120
+ if (!plan || plan.version !== 1) {
121
+ throw new Error('invalid npm update handoff plan version');
122
+ }
123
+ if (!Array.isArray(plan.command) || plan.command.length === 0 ||
124
+ plan.command.some((value) => typeof value !== 'string' || value.length === 0)) {
125
+ throw new Error('invalid npm update command');
126
+ }
127
+ if (!plan.latest_version || !plan.executable) {
128
+ throw new Error('incomplete npm update handoff plan');
129
+ }
130
+ }
131
+
132
+ function validateHandoffOwnership(ownership) {
133
+ if (!ownership || ownership.version !== 1) {
134
+ throw new Error('invalid npm update handoff ownership version');
135
+ }
136
+ const lockPath = String(ownership.lock_path || '').trim();
137
+ const lockToken = String(ownership.lock_token || '').trim();
138
+ if (Boolean(lockPath) !== Boolean(lockToken)) {
139
+ throw new Error('incomplete npm update lock ownership');
140
+ }
141
+ }
142
+
143
+ function normalizeVersion(value) {
144
+ return String(value || '').trim().replace(/^v/i, '');
145
+ }
146
+
147
+ async function defaultRunInstall(plan, signalCoordinator) {
148
+ const executable = plan.command[0];
149
+ const args = plan.command.slice(1);
150
+ if (process.platform === 'win32' && /\.(?:cmd|bat)$/i.test(executable)) {
151
+ const commandLine = String(plan.command_line || '').trim();
152
+ if (!commandLine) {
153
+ throw new Error('missing Windows npm command line');
154
+ }
155
+ return runCapturedProcess(
156
+ process.env.ComSpec || 'cmd.exe',
157
+ ['/d', '/s', '/c', commandLine],
158
+ { env: process.env },
159
+ signalCoordinator,
160
+ );
161
+ }
162
+ return runCapturedProcess(executable, args, { env: process.env }, signalCoordinator);
163
+ }
164
+
165
+ async function defaultVerifyVersion(executable, signalCoordinator) {
166
+ const result = await runCapturedProcess(
167
+ executable,
168
+ ['version', '--format', 'json'],
169
+ { env: process.env },
170
+ signalCoordinator,
171
+ );
172
+ if (result.signal || result.code !== 0) {
173
+ const detail = String(result.stderr || result.stdout || '').trim();
174
+ throw new Error(`updated Caelis did not start${detail ? `: ${detail}` : ''}`);
175
+ }
176
+ let payload;
177
+ try {
178
+ payload = JSON.parse(result.stdout);
179
+ } catch (err) {
180
+ throw new Error(`updated Caelis returned invalid version output: ${err.message}`);
181
+ }
182
+ return payload.version;
183
+ }
184
+
185
+ function installFailure(result) {
186
+ if (result.signal) {
187
+ return new Error(`npm install terminated by signal ${result.signal}`);
188
+ }
189
+ const detail = String(result.stderr || result.stdout || '').trim();
190
+ return new Error(
191
+ `npm install exited with code ${result.code}${detail ? `\n${detail}` : ''}`,
192
+ );
193
+ }
194
+
195
+ function removeOwnedLock(lockPath, lockToken) {
196
+ if (!lockPath || !lockToken) {
197
+ return;
198
+ }
199
+ let current;
200
+ try {
201
+ current = fs.readFileSync(lockPath, 'utf8').trim();
202
+ } catch (err) {
203
+ if (err && err.code === 'ENOENT') {
204
+ return;
205
+ }
206
+ throw err;
207
+ }
208
+ if (current !== String(lockToken).trim()) {
209
+ return;
210
+ }
211
+ fs.rmSync(lockPath, { force: true });
212
+ }
213
+
214
+ async function executeHandoffPlan(plan, options = {}) {
215
+ const stderr = options.stderr || process.stderr;
216
+ const stdout = options.stdout || process.stdout;
217
+ const status = options.status || createStatusRenderer(stderr);
218
+ const signalCoordinator = options.signalCoordinator;
219
+ const runInstall = options.runInstall ||
220
+ ((handoffPlan) => defaultRunInstall(handoffPlan, signalCoordinator));
221
+ const verifyVersion = options.verifyVersion ||
222
+ ((executable) => defaultVerifyVersion(executable, signalCoordinator));
223
+ let phase = 'install';
224
+
225
+ try {
226
+ validateHandoffPlan(plan);
227
+ status.start('Installing update with npm…');
228
+ const result = await runInstall(plan);
229
+ if (result.signal || result.code !== 0) {
230
+ throw installFailure(result);
231
+ }
232
+ status.succeed('npm install completed');
233
+
234
+ phase = 'verify';
235
+ status.start('Verifying updated Caelis…');
236
+ const installedVersion = await verifyVersion(plan.executable);
237
+ if (normalizeVersion(installedVersion) !== normalizeVersion(plan.latest_version)) {
238
+ throw new Error(
239
+ `expected ${plan.latest_version}, found ${installedVersion || 'unknown'}`,
240
+ );
241
+ }
242
+ status.succeed(`Verified Caelis ${plan.latest_version}`);
243
+ stdout.write(
244
+ // Keep this completion contract aligned with formatUpdateResult in
245
+ // internal/cli/update.go; Go is silent while a handoff is active.
246
+ `Caelis ${plan.latest_version} is ready ` +
247
+ `(updated from ${plan.current_version || 'unknown'} via npm).\n`,
248
+ );
249
+ return 0;
250
+ } catch (err) {
251
+ status.fail(phase === 'install' ? 'npm install failed' : 'Version verification failed');
252
+ throw err;
253
+ } finally {
254
+ status.stop();
255
+ }
256
+ }
257
+
258
+ function reserveHandoffDirectory(platform, options = {}) {
259
+ if (platform !== 'win32') {
260
+ return '';
261
+ }
262
+ const tmpdir = options.tmpdir || os.tmpdir;
263
+ const randomUUID = options.randomUUID || crypto.randomUUID;
264
+ return path.join(tmpdir(), `caelis-npm-update-${randomUUID()}`);
265
+ }
266
+
267
+ function readHandoffJSON(filePath, label) {
268
+ let data;
269
+ try {
270
+ data = fs.readFileSync(filePath, 'utf8');
271
+ } catch (err) {
272
+ throw new Error(`cannot read ${label}: ${err.message}`);
273
+ }
274
+ try {
275
+ return JSON.parse(data);
276
+ } catch (err) {
277
+ throw new Error(`invalid ${label}: ${err.message}`);
278
+ }
279
+ }
280
+
281
+ async function completeHandoffLifecycle(handoffDir, childResult, options = {}) {
282
+ if (!handoffDir || !fs.existsSync(handoffDir)) {
283
+ return childResult;
284
+ }
285
+ const executePlan = options.executePlan ||
286
+ ((plan) => executeHandoffPlan(plan, {
287
+ signalCoordinator: options.signalCoordinator,
288
+ }));
289
+ const removeLock = options.removeLock || removeOwnedLock;
290
+ const ownershipPath = path.join(handoffDir, handoffOwnershipName);
291
+ const planPath = path.join(handoffDir, handoffPlanName);
292
+ let ownership;
293
+
294
+ try {
295
+ if (fs.existsSync(ownershipPath)) {
296
+ const candidate = readHandoffJSON(ownershipPath, 'npm update handoff ownership');
297
+ validateHandoffOwnership(candidate);
298
+ ownership = candidate;
299
+ }
300
+ const planExists = fs.existsSync(planPath);
301
+ if (!planExists) {
302
+ if (ownership && !childResult.signal && childResult.code === 0) {
303
+ throw new Error('npm update handoff plan was not published');
304
+ }
305
+ return childResult;
306
+ }
307
+ if (!ownership) {
308
+ throw new Error('npm update handoff ownership was not published');
309
+ }
310
+ if (childResult.signal || childResult.code !== 0) {
311
+ return childResult;
312
+ }
313
+ const plan = readHandoffJSON(planPath, 'npm update handoff plan');
314
+ const code = await executePlan(plan);
315
+ return { code, signal: null };
316
+ } finally {
317
+ try {
318
+ if (ownership) {
319
+ removeLock(ownership.lock_path, ownership.lock_token);
320
+ }
321
+ } finally {
322
+ fs.rmSync(handoffDir, { recursive: true, force: true });
323
+ }
324
+ }
325
+ }
326
+
327
+ async function completeHandoff(handoffDir, childResult, options = {}) {
328
+ try {
329
+ return await completeHandoffLifecycle(handoffDir, childResult, options);
330
+ } catch (err) {
331
+ if (err instanceof UpdateHandoffError) {
332
+ throw err;
333
+ }
334
+ throw new UpdateHandoffError(err.message, { cause: err });
335
+ }
336
+ }
337
+
338
+ module.exports = {
339
+ UpdateHandoffError,
340
+ completeHandoff,
341
+ createStatusRenderer,
342
+ executeHandoffPlan,
343
+ handoffEnvironment,
344
+ handoffOwnershipName,
345
+ handoffPlanName,
346
+ normalizeVersion,
347
+ removeOwnedLock,
348
+ reserveHandoffDirectory,
349
+ validateHandoffPlan,
350
+ validateHandoffOwnership,
351
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@caelis/caelis",
3
- "version": "0.28.0",
3
+ "version": "0.30.0",
4
4
  "description": "caelis CLI distributed via npm",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -16,6 +16,7 @@
16
16
  },
17
17
  "files": [
18
18
  "bin/caelis.js",
19
+ "lib/update-handoff.js",
19
20
  "README.md",
20
21
  "LICENSE"
21
22
  ],
@@ -29,18 +30,19 @@
29
30
  "arm64"
30
31
  ],
31
32
  "scripts": {
33
+ "test": "node --test ./test/*.test.js",
32
34
  "prepare": "node ./scripts/prepare.mjs",
33
35
  "set-version": "node ./scripts/set-version.mjs",
34
36
  "stage-release": "node ./scripts/stage-release.mjs",
35
37
  "publish-release": "node ./scripts/publish-release.mjs"
36
38
  },
37
39
  "optionalDependencies": {
38
- "@caelis/caelis-darwin-arm64": "0.28.0",
39
- "@caelis/caelis-darwin-x64": "0.28.0",
40
- "@caelis/caelis-linux-arm64": "0.28.0",
41
- "@caelis/caelis-linux-x64": "0.28.0",
42
- "@caelis/caelis-windows-arm64": "0.28.0",
43
- "@caelis/caelis-windows-x64": "0.28.0"
40
+ "@caelis/caelis-darwin-arm64": "0.30.0",
41
+ "@caelis/caelis-darwin-x64": "0.30.0",
42
+ "@caelis/caelis-linux-arm64": "0.30.0",
43
+ "@caelis/caelis-linux-x64": "0.30.0",
44
+ "@caelis/caelis-windows-arm64": "0.30.0",
45
+ "@caelis/caelis-windows-x64": "0.30.0"
44
46
  },
45
47
  "publishConfig": {
46
48
  "access": "public",