praxis-agent 0.42.1 → 0.43.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/README.md CHANGED
@@ -45,6 +45,7 @@ praxis --version
45
45
 
46
46
  Release tarballs, SBOMs, SHA-256 checksums, and build attestations are attached
47
47
  to every [GitHub release](https://github.com/Forest-Isle/Praxis/releases).
48
+ Use `praxis update` for transactional self-updates; see [Getting Started](https://github.com/Forest-Isle/Praxis/blob/main/docs/GETTING_STARTED.md) for recovery and verification details.
48
49
 
49
50
  ## Quick start
50
51
 
@@ -204,6 +205,9 @@ troubleshooting. Run `praxis --help` for the authoritative command surface.
204
205
  - **Provider-neutral models** — native Anthropic Messages and OpenAI-compatible
205
206
  streaming adapters with explicit capability checks, metering controls, and
206
207
  a bounded absolute deadline for every provider attempt.
208
+ - **Transactional self-update** — `praxis update` verifies the package before
209
+ installing it, rejects concurrent updates, and can roll back after an
210
+ interruption or crash.
207
211
 
208
212
  Detailed feature status and executable evidence live in the
209
213
  [parity matrix](https://github.com/Forest-Isle/Praxis/blob/main/docs/PARITY_MATRIX.md),
@@ -174,6 +174,7 @@ export interface CliDependencies extends InteractiveServiceFactory {
174
174
  operation: 'install' | 'update';
175
175
  target?: string;
176
176
  force?: boolean;
177
+ signal?: AbortSignal;
177
178
  }) => Promise<SelfUpdateResult>;
178
179
  }
179
180
  /**
@@ -2788,7 +2788,7 @@ async function executeDoctorCommand(args, invocation, io) {
2788
2788
  }
2789
2789
  return report.ok ? 0 : 1;
2790
2790
  }
2791
- async function executeSelfUpdateCommand(argv, io, dependencies) {
2791
+ async function executeSelfUpdateCommand(argv, io, dependencies, signal) {
2792
2792
  const command = argv[0];
2793
2793
  const values = [];
2794
2794
  for (let index = 1; index < argv.length; index += 1) {
@@ -2812,6 +2812,7 @@ async function executeSelfUpdateCommand(argv, io, dependencies) {
2812
2812
  const runtimeSettings = await loadRuntimeSettings({ configRoot, statePath });
2813
2813
  const result = await dependencies.selfUpdate?.({
2814
2814
  operation: 'update',
2815
+ ...(signal ? { signal } : {}),
2815
2816
  ...(runtimeSettings.autoUpdatesChannel === 'latest'
2816
2817
  ? {}
2817
2818
  : { target: runtimeSettings.autoUpdatesChannel }),
@@ -2836,6 +2837,7 @@ async function executeSelfUpdateCommand(argv, io, dependencies) {
2836
2837
  throw new Error('install accepts at most one target');
2837
2838
  const result = await dependencies.selfUpdate?.({
2838
2839
  operation: 'install',
2840
+ ...(signal ? { signal } : {}),
2839
2841
  force,
2840
2842
  ...(targets[0] === undefined ? {} : { target: targets[0] }),
2841
2843
  });
@@ -4600,7 +4602,7 @@ async function execute(argv, io, dependencies, signal) {
4600
4602
  return executeProjectPurgeCommand(['project', 'purge', ...prefixedPurgeOptions, ...special.args.slice(2)], io);
4601
4603
  }
4602
4604
  if (['install', 'update', 'upgrade'].includes(special.args[0] ?? '')) {
4603
- return executeSelfUpdateCommand([special.args[0], ...special.args.slice(1)], io, dependencies);
4605
+ return executeSelfUpdateCommand([special.args[0], ...special.args.slice(1)], io, dependencies, signal);
4604
4606
  }
4605
4607
  if (printCommandHelp(argv, io))
4606
4608
  return 0;
@@ -0,0 +1,39 @@
1
+ export interface SelfUpdateLayout {
2
+ packageRoot: string;
3
+ globalNodeModulesRoot: string;
4
+ globalPrefix: string;
5
+ binPath: string;
6
+ }
7
+ export interface TransactionRunnerOptions {
8
+ cwd?: string;
9
+ env?: NodeJS.ProcessEnv;
10
+ timeout?: number;
11
+ maxBuffer?: number;
12
+ signal?: AbortSignal;
13
+ }
14
+ export type TransactionRunner = (executable: string, args: readonly string[], options: TransactionRunnerOptions) => Promise<{
15
+ stdout: string;
16
+ stderr: string;
17
+ }>;
18
+ export interface SelfUpdateTransactionOptions {
19
+ packageName: string;
20
+ target: string;
21
+ force: boolean;
22
+ npmExecutable: string;
23
+ timeoutMs: number;
24
+ signal?: AbortSignal;
25
+ run: TransactionRunner;
26
+ layout?: SelfUpdateLayout;
27
+ }
28
+ export interface SelfUpdateTransactionResult {
29
+ output: string;
30
+ }
31
+ /** Transactional updater implementation. */
32
+ export declare function runSelfUpdateTransaction(options: SelfUpdateTransactionOptions): Promise<SelfUpdateTransactionResult>;
33
+ export declare function generateLauncherSource(layout: SelfUpdateLayout, lockPath: string, journal: string): string;
34
+ export declare function validateSelfUpdateLayout(layout: SelfUpdateLayout): SelfUpdateLayout;
35
+ export declare function checksum(bytes: Uint8Array): {
36
+ sha1: string;
37
+ sha512: string;
38
+ };
39
+ //# sourceMappingURL=self-update-transaction.d.ts.map
@@ -0,0 +1,356 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { access, chmod, mkdir, mkdtemp, open, readFile, rename, rm, stat, symlink, } from 'node:fs/promises';
3
+ import { dirname, basename, join, resolve } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { ExclusiveFileLease } from '../platform/exclusive-file-lease.js';
6
+ /** Transactional updater implementation. */
7
+ export async function runSelfUpdateTransaction(options) {
8
+ if (process.platform === 'win32')
9
+ throw new Error('self-update is unsupported on Windows');
10
+ options.signal?.throwIfAborted();
11
+ const layout = validateSelfUpdateLayout(options.layout ?? deriveLayout());
12
+ if (layout.packageRoot !==
13
+ join(layout.globalNodeModulesRoot, options.packageName))
14
+ throw new Error('current package is not installed in the requested global layout');
15
+ const parent = dirname(layout.packageRoot);
16
+ const lockPath = `${layout.packageRoot}.update.lock`;
17
+ let owner;
18
+ try {
19
+ owner = await new ExclusiveFileLease(lockPath).tryAcquire();
20
+ }
21
+ catch (error) {
22
+ throw normalizeTransactionError(error);
23
+ }
24
+ if (!owner)
25
+ throw new Error('Praxis update already in progress');
26
+ let backup = '';
27
+ let staging = '';
28
+ const journal = `${layout.packageRoot}.update.journal`;
29
+ try {
30
+ await validateCandidate(layout.packageRoot, options.packageName);
31
+ options.signal?.throwIfAborted();
32
+ const spec = `${options.packageName}@${options.target}`;
33
+ const view = await options.run(options.npmExecutable, ['view', spec, 'version', 'dist', '--json'], runnerOptions(options));
34
+ const metadata = parseView(view.stdout, options.packageName);
35
+ staging = await mkdtemp(join(parent, '.praxis-update-'));
36
+ await chmod(staging, 0o700);
37
+ const download = join(staging, 'download');
38
+ await mkdir(download, { recursive: true, mode: 0o700 });
39
+ await chmod(download, 0o700);
40
+ const packed = await options.run(options.npmExecutable, [
41
+ 'pack',
42
+ spec,
43
+ '--ignore-scripts',
44
+ '--json',
45
+ '--pack-destination',
46
+ download,
47
+ ], runnerOptions(options));
48
+ const pack = parsePack(packed.stdout, metadata, download, options.packageName);
49
+ const bytes = await readFile(pack.path);
50
+ const sums = checksum(bytes);
51
+ if (sums.sha512 !== metadata.integrity || sums.sha1 !== metadata.shasum)
52
+ throw new Error('package integrity/checksum mismatch');
53
+ const stagingPrefix = join(staging, 'prefix');
54
+ await mkdir(stagingPrefix, { recursive: true, mode: 0o700 });
55
+ await chmod(stagingPrefix, 0o700);
56
+ const installArgs = [
57
+ 'install',
58
+ '--global',
59
+ '--prefix',
60
+ stagingPrefix,
61
+ '--no-fund',
62
+ '--no-audit',
63
+ '--ignore-scripts',
64
+ ...(options.force ? ['--force'] : []),
65
+ pack.path,
66
+ ];
67
+ await options.run(options.npmExecutable, installArgs, runnerOptions(options));
68
+ const candidate = join(stagingPrefix, 'lib', 'node_modules', options.packageName);
69
+ await validateCandidate(candidate, options.packageName, metadata.version);
70
+ await gate(options, join(candidate, 'dist', 'cli.js'), metadata.version);
71
+ options.signal?.throwIfAborted();
72
+ backup = `${layout.packageRoot}.update-backup-${randomUUID()}`;
73
+ await installLauncher(layout, lockPath, journal);
74
+ await writeJournal(journal, {
75
+ version: 1,
76
+ root: layout.packageRoot,
77
+ backup,
78
+ staging,
79
+ targetVersion: metadata.version,
80
+ phase: 'prepared',
81
+ });
82
+ await rename(layout.packageRoot, backup);
83
+ await rename(candidate, layout.packageRoot);
84
+ await syncDirectory(parent);
85
+ await writeJournal(journal, {
86
+ version: 1,
87
+ root: layout.packageRoot,
88
+ backup,
89
+ staging,
90
+ targetVersion: metadata.version,
91
+ phase: 'candidate',
92
+ });
93
+ await gate(options, join(layout.packageRoot, 'dist', 'cli.js'), metadata.version);
94
+ await cleanupCompletedTransaction(backup, staging, journal);
95
+ return { output: 'completed' };
96
+ }
97
+ catch (error) {
98
+ const failure = normalizeTransactionError(error);
99
+ if (backup) {
100
+ try {
101
+ await rollback(layout.packageRoot, backup, staging, journal);
102
+ }
103
+ catch (recovery) {
104
+ const recoveryFailure = normalizeTransactionError(recovery);
105
+ throw new Error(`${failure instanceof Error ? failure.message : String(failure)}; recovery failed: ${recoveryFailure instanceof Error ? recoveryFailure.message : String(recoveryFailure)}`, { cause: failure });
106
+ }
107
+ }
108
+ else if (staging)
109
+ await cleanupCompletedTransaction('', staging, journal);
110
+ throw failure;
111
+ }
112
+ finally {
113
+ await releaseLease(owner);
114
+ }
115
+ }
116
+ function deriveLayout() {
117
+ const packageRoot = resolve(process.env.PRAXIS_SELF_UPDATE_ROOT ??
118
+ dirname(fileURLToPath(import.meta.url)), process.env.PRAXIS_SELF_UPDATE_ROOT ? '.' : '../..');
119
+ const globalNodeModulesRoot = dirname(packageRoot);
120
+ const globalPrefix = dirname(dirname(globalNodeModulesRoot));
121
+ return {
122
+ packageRoot,
123
+ globalNodeModulesRoot,
124
+ globalPrefix,
125
+ binPath: join(globalPrefix, 'bin', 'praxis'),
126
+ };
127
+ }
128
+ function runnerOptions(options) {
129
+ return {
130
+ env: process.env,
131
+ timeout: options.timeoutMs,
132
+ maxBuffer: 4 * 1024 * 1024,
133
+ ...(options.signal ? { signal: options.signal } : {}),
134
+ };
135
+ }
136
+ function parseJson(text) {
137
+ try {
138
+ return JSON.parse(text);
139
+ }
140
+ catch {
141
+ throw new Error('invalid npm metadata');
142
+ }
143
+ }
144
+ function semver(value) {
145
+ return (typeof value === 'string' &&
146
+ /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-(?:0|[1-9]\d*|[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9]\d*|[A-Za-z-][0-9A-Za-z-]*))*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/u.test(value));
147
+ }
148
+ function parseView(text, name) {
149
+ const value = parseJson(text);
150
+ const dist = value.dist;
151
+ if (!semver(value.version) ||
152
+ !dist ||
153
+ typeof dist.tarball !== 'string' ||
154
+ !/^https:\/\//u.test(dist.tarball) ||
155
+ typeof dist.integrity !== 'string' ||
156
+ !/^sha512-[A-Za-z0-9+/]+=*$/u.test(dist.integrity) ||
157
+ typeof dist.shasum !== 'string' ||
158
+ !/^[0-9a-f]{40}$/iu.test(dist.shasum))
159
+ throw new Error(`invalid metadata for ${name}`);
160
+ return {
161
+ version: value.version,
162
+ integrity: dist.integrity,
163
+ shasum: dist.shasum.toLowerCase(),
164
+ tarball: dist.tarball,
165
+ };
166
+ }
167
+ function parsePack(text, metadata, dir, packageName) {
168
+ const value = parseJson(text);
169
+ if (!Array.isArray(value) || value.length !== 1)
170
+ throw new Error('invalid npm pack metadata');
171
+ const record = value[0];
172
+ if (record.version !== metadata.version ||
173
+ record.name !== packageName ||
174
+ record.integrity !== metadata.integrity ||
175
+ String(record.shasum).toLowerCase() !== metadata.shasum ||
176
+ typeof record.filename !== 'string' ||
177
+ basename(record.filename) !== record.filename ||
178
+ !record.filename.endsWith('.tgz'))
179
+ throw new Error('npm pack metadata mismatch');
180
+ return { path: join(dir, record.filename) };
181
+ }
182
+ async function validateCandidate(root, name, expectedVersion) {
183
+ const manifest = parseJson(await readFile(join(root, 'package.json'), 'utf8'));
184
+ const cli = await stat(join(root, 'dist', 'cli.js'));
185
+ if (manifest.name !== name ||
186
+ !semver(manifest.version) ||
187
+ (expectedVersion !== undefined && manifest.version !== expectedVersion) ||
188
+ !cli.isFile())
189
+ throw new Error('staged package validation failed');
190
+ return manifest.version;
191
+ }
192
+ async function gate(options, cli, version) {
193
+ const result = await options.run(process.execPath, [cli, '--version'], runnerOptions(options));
194
+ if (result.stdout.trim() !== version)
195
+ throw new Error('candidate version gate failed');
196
+ }
197
+ async function writeJournal(path, value) {
198
+ const tmp = `${path}.${randomUUID()}.tmp`;
199
+ try {
200
+ const handle = await open(tmp, 'wx', 0o600);
201
+ try {
202
+ await handle.writeFile(JSON.stringify(value));
203
+ await handle.chmod(0o600);
204
+ await handle.sync();
205
+ }
206
+ finally {
207
+ await handle.close();
208
+ }
209
+ await rename(tmp, path);
210
+ await syncDirectory(dirname(path));
211
+ }
212
+ finally {
213
+ await rm(tmp, { force: true });
214
+ }
215
+ }
216
+ async function rollback(root, backup, staging, journal) {
217
+ if (await exists(backup)) {
218
+ let failed = '';
219
+ if (await exists(root)) {
220
+ failed = join(staging, 'failed-candidate');
221
+ await rename(root, failed);
222
+ }
223
+ await rename(backup, root);
224
+ await syncDirectory(dirname(root));
225
+ if (failed)
226
+ await rm(failed, { recursive: true, force: true });
227
+ }
228
+ else if (!(await exists(root))) {
229
+ throw new Error('update backup is unavailable');
230
+ }
231
+ await cleanupCompletedTransaction('', staging, journal);
232
+ }
233
+ async function cleanupCompletedTransaction(backup, staging, journal) {
234
+ try {
235
+ if (backup)
236
+ await rm(backup, { recursive: true, force: true });
237
+ if (staging)
238
+ await rm(staging, { recursive: true, force: true });
239
+ await rm(journal, { force: true });
240
+ await syncDirectory(dirname(journal));
241
+ }
242
+ catch {
243
+ // The external launcher retries validated cleanup once the lease is gone.
244
+ }
245
+ }
246
+ async function syncDirectory(path) {
247
+ const handle = await open(path, 'r');
248
+ try {
249
+ await handle.sync();
250
+ }
251
+ finally {
252
+ await handle.close();
253
+ }
254
+ }
255
+ async function exists(path) {
256
+ try {
257
+ await access(path);
258
+ return true;
259
+ }
260
+ catch {
261
+ return false;
262
+ }
263
+ }
264
+ function normalizeTransactionError(error) {
265
+ if (typeof error !== 'object' || error === null)
266
+ return error;
267
+ const systemError = error;
268
+ if (typeof systemError.path === 'string' ||
269
+ typeof systemError.dest === 'string' ||
270
+ typeof systemError.syscall === 'string') {
271
+ return new Error('self-update filesystem transaction failed', {
272
+ cause: error,
273
+ });
274
+ }
275
+ return error;
276
+ }
277
+ async function releaseLease(owner) {
278
+ try {
279
+ await owner.release();
280
+ }
281
+ catch (error) {
282
+ throw normalizeTransactionError(error);
283
+ }
284
+ }
285
+ async function installLauncher(layout, lockPath, journal) {
286
+ const launcher = `${layout.packageRoot}.launcher.mjs`;
287
+ const source = generateLauncherSource(layout, lockPath, journal);
288
+ const tmp = `${launcher}.${randomUUID()}.tmp`;
289
+ try {
290
+ const handle = await open(tmp, 'wx', 0o700);
291
+ try {
292
+ await handle.writeFile(source);
293
+ await handle.chmod(0o700);
294
+ await handle.sync();
295
+ }
296
+ finally {
297
+ await handle.close();
298
+ }
299
+ await rename(tmp, launcher);
300
+ await syncDirectory(dirname(launcher));
301
+ const link = `${layout.binPath}.${randomUUID()}.tmp`;
302
+ try {
303
+ await symlink(launcher, link);
304
+ await rename(link, layout.binPath);
305
+ await syncDirectory(dirname(layout.binPath));
306
+ }
307
+ finally {
308
+ await rm(link, { force: true });
309
+ }
310
+ }
311
+ finally {
312
+ await rm(tmp, { force: true });
313
+ }
314
+ }
315
+ export function generateLauncherSource(layout, lockPath, journal) {
316
+ return `#!/usr/bin/env node
317
+ import { existsSync, readFileSync, renameSync, rmSync } from 'node:fs';
318
+ import { dirname, join, resolve } from 'node:path';
319
+ const root=${JSON.stringify(layout.packageRoot)}, parent=${JSON.stringify(dirname(layout.packageRoot))}, backupPrefix=${JSON.stringify(`${layout.packageRoot}.update-backup-`)}, stagingPrefix=${JSON.stringify(`${dirname(layout.packageRoot)}/.praxis-update-`)}, lock=${JSON.stringify(lockPath)}, journal=${JSON.stringify(journal)};
320
+ const alive=(p)=>{try{process.kill(p,0);return true}catch(e){return e.code==='EPERM'}};
321
+ const owner=()=>{try{const j=JSON.parse(readFileSync(lock,'utf8'));return j&&j.version===1&&Number.isSafeInteger(j.pid)&&j.pid>0&&typeof j.token==='string'&&/^[A-Za-z0-9_-]{1,128}$/.test(j.token)&&typeof j.createdAt==='string'&&alive(j.pid)}catch{return false}};
322
+ const sibling=(value,prefix)=>typeof value==='string'&&value===resolve(value)&&dirname(value)===parent&&value.startsWith(prefix)&&value.length>prefix.length;
323
+ const valid=(j)=>j&&j.version===1&&j.root===root&&sibling(j.backup,backupPrefix)&&sibling(j.staging,stagingPrefix)&&['prepared','backup','candidate'].includes(j.phase)&&typeof j.targetVersion==='string';
324
+ const liveOwner=owner();
325
+ if(liveOwner&&!existsSync(root)){let n=0;while(n++<20&&!existsSync(root)){await new Promise(r=>setTimeout(r,25))}if(!existsSync(root))throw new Error('Praxis update already in progress');
326
+ } else if(!liveOwner&&existsSync(journal)){const j=JSON.parse(readFileSync(journal,'utf8'));if(!valid(j))throw new Error('invalid update journal');if(!existsSync(root)&&existsSync(j.backup))renameSync(j.backup,root);if(existsSync(root)){if(existsSync(j.backup))rmSync(j.backup,{recursive:true,force:true});rmSync(j.staging,{recursive:true,force:true});rmSync(journal,{force:true});}}
327
+ if(!existsSync(root)) throw new Error('Praxis update recovery failed');
328
+ process.env.PRAXIS_SELF_UPDATE_ROOT=root; const controller=new AbortController(); const cancel=()=>controller.abort(); process.on('SIGINT',cancel); process.on('SIGTERM',cancel); try { const mod=await import(join(root,'dist','cli.js')); if(typeof mod.run!=='function') throw new Error('CLI entrypoint unavailable'); process.exitCode=await mod.run(process.argv.slice(2),undefined,undefined,controller.signal); } finally { process.removeListener('SIGINT',cancel); process.removeListener('SIGTERM',cancel); }
329
+ `;
330
+ }
331
+ export function validateSelfUpdateLayout(layout) {
332
+ const packageRoot = resolve(layout.packageRoot);
333
+ const modules = resolve(layout.globalNodeModulesRoot);
334
+ const prefix = resolve(layout.globalPrefix);
335
+ const binPath = resolve(layout.binPath);
336
+ if (dirname(packageRoot) !== modules ||
337
+ dirname(modules) !== join(prefix, 'lib')) {
338
+ throw new Error('unsupported global npm installation layout');
339
+ }
340
+ if (binPath !== join(prefix, 'bin', 'praxis')) {
341
+ throw new Error('unsupported global npm bin layout');
342
+ }
343
+ return {
344
+ packageRoot,
345
+ globalNodeModulesRoot: modules,
346
+ globalPrefix: prefix,
347
+ binPath,
348
+ };
349
+ }
350
+ export function checksum(bytes) {
351
+ return {
352
+ sha1: createHash('sha1').update(bytes).digest('hex'),
353
+ sha512: `sha512-${createHash('sha512').update(bytes).digest('base64')}`,
354
+ };
355
+ }
356
+ //# sourceMappingURL=self-update-transaction.js.map
@@ -1,3 +1,4 @@
1
+ import { type SelfUpdateLayout } from './self-update-transaction.js';
1
2
  export type SelfUpdateOperation = 'install' | 'update';
2
3
  export interface SelfUpdateOptions {
3
4
  operation: SelfUpdateOperation;
@@ -6,6 +7,8 @@ export interface SelfUpdateOptions {
6
7
  packageName?: string;
7
8
  npmExecutable?: string;
8
9
  timeoutMs?: number;
10
+ signal?: AbortSignal;
11
+ layout?: SelfUpdateLayout;
9
12
  run?: SelfUpdateRunner;
10
13
  }
11
14
  export interface SelfUpdateRunnerOptions {
@@ -13,6 +16,7 @@ export interface SelfUpdateRunnerOptions {
13
16
  env?: NodeJS.ProcessEnv;
14
17
  timeout?: number;
15
18
  maxBuffer?: number;
19
+ signal?: AbortSignal;
16
20
  }
17
21
  export type SelfUpdateRunner = (executable: string, args: readonly string[], options: SelfUpdateRunnerOptions) => Promise<{
18
22
  stdout: string;
@@ -1,20 +1,27 @@
1
1
  import { execFile } from 'node:child_process';
2
2
  import { promisify } from 'node:util';
3
+ import { runSelfUpdateTransaction, } from './self-update-transaction.js';
3
4
  const execFileAsync = promisify(execFile);
4
5
  const DEFAULT_PACKAGE_NAME = 'praxis-agent';
5
6
  const DEFAULT_TIMEOUT_MS = 120_000;
6
- const MAX_OUTPUT_BYTES = 4 * 1024 * 1024;
7
7
  const runCommand = async (executable, args, options) => {
8
- const result = await execFileAsync(executable, [...args], {
9
- cwd: options.cwd,
10
- env: options.env,
11
- timeout: options.timeout,
12
- maxBuffer: options.maxBuffer,
13
- });
14
- return {
15
- stdout: String(result.stdout ?? ''),
16
- stderr: String(result.stderr ?? ''),
17
- };
8
+ try {
9
+ const result = await execFileAsync(executable, [...args], {
10
+ cwd: options.cwd,
11
+ env: options.env,
12
+ timeout: options.timeout,
13
+ maxBuffer: options.maxBuffer,
14
+ signal: options.signal,
15
+ });
16
+ return {
17
+ stdout: String(result.stdout ?? ''),
18
+ stderr: String(result.stderr ?? ''),
19
+ };
20
+ }
21
+ catch (error) {
22
+ options.signal?.throwIfAborted();
23
+ throw new Error('self-update subprocess failed', { cause: error });
24
+ }
18
25
  };
19
26
  function validPackageName(value) {
20
27
  return /^(?:@[^/\s]+\/)?[^/\s]+$/u.test(value);
@@ -30,10 +37,6 @@ function requireTarget(value) {
30
37
  }
31
38
  return target;
32
39
  }
33
- function commandOutput(stdout, stderr) {
34
- const output = `${stdout.trim()}${stderr.trim() ? `\n${stderr.trim()}` : ''}`;
35
- return output.length > 0 ? output : 'completed';
36
- }
37
40
  export async function runSelfUpdate(options) {
38
41
  const packageName = options.packageName ?? DEFAULT_PACKAGE_NAME;
39
42
  if (!validPackageName(packageName)) {
@@ -56,10 +59,16 @@ export async function runSelfUpdate(options) {
56
59
  const runner = options.run ?? runCommand;
57
60
  let result;
58
61
  try {
59
- result = await runner(npmExecutable, args, {
60
- env: process.env,
61
- timeout: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
62
- maxBuffer: MAX_OUTPUT_BYTES,
62
+ const transactionRunner = runner;
63
+ result = await runSelfUpdateTransaction({
64
+ packageName,
65
+ target,
66
+ force,
67
+ npmExecutable,
68
+ timeoutMs: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
69
+ run: transactionRunner,
70
+ ...(options.signal ? { signal: options.signal } : {}),
71
+ ...(options.layout ? { layout: options.layout } : {}),
63
72
  });
64
73
  }
65
74
  catch (error) {
@@ -75,7 +84,7 @@ export async function runSelfUpdate(options) {
75
84
  target,
76
85
  force,
77
86
  command: [npmExecutable, ...args],
78
- output: commandOutput(result.stdout, result.stderr),
87
+ output: result.output,
79
88
  };
80
89
  }
81
90
  //# sourceMappingURL=self-update.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "praxis-agent",
3
- "version": "0.42.1",
3
+ "version": "0.43.0",
4
4
  "description": "Local-first, single-user general agent for the command line.",
5
5
  "license": "MIT",
6
6
  "author": "wuqisen",