@ai-ecoverse/slicc-kernel 1.75.0 → 1.76.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
@@ -271,14 +271,14 @@ The module's default export (or its `main` export) is called with a context and
271
271
  - `/dev`, in `open`, `fs` and their `sync` forms, as a WASI or Emscripten program sees it, with the same modes and owners as under `users`: `null`, `zero`, `full`, `random`, `urandom` and `tty` (character devices, `0666`, root's), `stdin`, `stdout` and `stderr` (links to `/proc/self/fd/0`, `1`, `2`), `fd` (a link to `/proc/self/fd`, so `/dev/fd/N` is descriptor N) and the kernel's terminals `/dev/ttyN` and `/dev/pts/N` (with users on, owned by the session's user or the master's opener, `0620`). `/dev/shm` and `/dev/ptmx` aren't there. `stat` through `/dev/stdin`, `/dev/stdout` or `/dev/fd/N` describes the program's own descriptor (the file it is redirected to, a pipe, a terminal), `readlink` of `/dev/fd/N` names it (`pipe:[N]` and `socket:[N]` for a pipe or a socket), and a closed descriptor is `ENOENT`. Opening a device gives a kernel descriptor on it; opening `/dev/std*` or `/dev/fd/N` opens the file again with the flags asked for when the descriptor is on a file (as Linux reopens `/proc/self/fd/N`, so `append` appends and `truncate` truncates), and otherwise duplicates the descriptor; `/dev/tty` is the program's controlling terminal (`ENXIO` without one); and `/dev/ttyN` and `/dev/pts/N` are opened by name through the kernel, so a terminal that isn't the user's is `EACCES` under `enforce`. A file on anything but a regular file reads and writes as a stream (the position is ignored) and only in the directions it was opened for (`EBADF` otherwise), its `size()` is 0 and `truncate` is `EINVAL`. `readFile` reads a device or a descriptor to its end, up to 64 MiB, and fails with `EFBIG` past that (where Node's `readFile` throws `ERR_FS_FILE_TOO_LARGE`), so `readFile('/dev/zero')` fails instead of hanging, while a bounded `read` of it works. Other paths pay only a prefix check: they make the same kernel calls as before. Creating, removing or renaming names in `/dev` goes to the root file system as before;
272
272
  - `signals`: `on(signal, handler)` catches a signal (by number or as `'SIGINT'`), `ignore(signal)` ignores it and `reset(signal)` restores its default action. A call blocked in a read or write is restarted after a handler runs; signals without a handler keep their default actions, so an uncaught `SIGINT` ends the program with 130 and `SIGKILL` always does;
273
273
  - `sync`: `read`, `write`, `close`, `open` (whose files' methods are synchronous too) and `fs`, made synchronously, for code that cannot `await`, such as a compiled program's file system or a `readFileSync`. A synchronous call holds the worker until the kernel answers, so nothing else of the program runs meanwhile, timers and callbacks included; a signal that arrives during a blocked read or write runs its handler, and the call is then restarted. Descriptors are shared with the rest of the context, so a file opened with `sync.open` can be closed with `close`, and the other way around; `sync.run` and `sync.exit` are described next;
274
- - `sync.run({ argv, env, cwd, pgid, session, input, stdin, stdout, stderr, maxBuffer, timeout, killSignal })` runs a command to its end and answers `{ pid, status, signal, stdout, stderr, error }`, as Node's `spawnSync` does. `pgid` and `session` work as for `spawn` below; a group that doesn't exist or is another session's answers `error: { code: 'ESRCH' }` or `'EPERM'`, and a bad option throws `EINVAL`. It suits `execSync`, `spawnSync` and `execFileSync`. The kernel does the whole run while the program's worker waits; other processes go on.
274
+ - `sync.run({ argv, argv0, env, cwd, pgid, session, input, stdin, stdout, stderr, maxBuffer, timeout, killSignal })` runs a command to its end and answers `{ pid, status, signal, stdout, stderr, error }`, as Node's `spawnSync` does. `pgid` and `session` work as for `spawn` below; a group that doesn't exist or is another session's answers `error: { code: 'ESRCH' }` or `'EPERM'`, and a bad option throws `EINVAL`. It suits `execSync`, `spawnSync` and `execFileSync`. `argv0` works as for `spawn` below. The kernel does the whole run while the program's worker waits; other processes go on.
275
275
  - Each stdio is `'pipe'` (the default), `'ignore'`, `'inherit'` (the program's own, a terminal included), or one of the program's descriptors by number, which the child gets as it is (two slots on one descriptor share it, so their writes keep their order). `stderr: 'stdout'` sends stderr to whatever stdout is, the stdout capture included, in the order the child writes. With `'pipe'`, stdin gets `input` (bytes, or a string as UTF-8) and then its end, so `cat` stops; `stdout` and `stderr` come back as bytes and are absent for the other choices. Both are drained while the child runs, so a child that writes more than a pipe holds never stalls. As in Node, the call returns once the child has exited and every captured stream has reached its end, so output a background grandchild writes later is still collected; `timeout` bounds that wait too, so, as with Node's `spawnSync`, a run blocks for as long as a background process keeps a captured stream open unless `timeout` is set; pass one for a command that may leave something in the background. Once the outcome is decided (the end of output, an overflow or a timeout), the captured streams are closed, so a writer still left on them gets `EPIPE` (and `SIGPIPE`) instead of running on.
276
276
  - `status` is the exit status, or `null` with `signal` set when a signal ended the child. A non-zero status is not an error. A command that cannot start (it does not exist, or is not executable) answers `error: { code: 'ENOENT' }` (or the errno), `status: null` and `pid: 0` instead of throwing.
277
277
  - More than `maxBuffer` bytes on `stdout` or `stderr` (1 MiB by default) sends the child `killSignal` (`SIGTERM` by default), keeps the first `maxBuffer` bytes, and answers `error: { code: 'ENOBUFS' }`. The kernel keeps at most 64 MiB of each stream whatever `maxBuffer` says, `Infinity` included, since the buffers live in the kernel worker that all processes share. `timeout` (ms) sends `killSignal` when it runs out and answers `error: { code: 'ETIMEDOUT' }`. A child that ignores `killSignal` is still waited for, as in Node.
278
278
  - The child is in the program's process group, so `^C` reaches it. The kernel reaps it, so it leaves no zombie, and its CPU time counts as the program's children's. A signal the program catches does not cut the run short; its handler runs when the call returns. A signal that kills the program kills it as usual;
279
279
  - `sync.kill(pid, signal)`, a synchronous `kill`. When the signal reaches the program itself (its own pid, `0` for its process group, or `-pgid` of its own group) and its action is to end the program (no handler, not ignored), the call never returns: the program's writes reach their descriptors and its terminal modes are put back, as for `sync.exit`, then the rest of the group is signalled and the program ends by the signal, so a parent's `wait` and `sync.run` report `signal`, as Node does for `process.kill(process.pid)`. A signal with a handler runs it when the call returns, an ignored one does nothing, and a stop signal stops the program. A signal whose default action dumps core (`SIGQUIT`, `SIGABRT`, `SIGSEGV`, …) ends it the same way; the kernel writes no core and sets no core flag in the wait status. `-1` signals every process the program may signal except itself and pid 1, as on Linux, and returns (`ESRCH` when there was none). Without users, every process runs as root, so `kill -9 -1` from any program ends every other process; with users it reaches only what the sender may signal (its own uid's processes, or all of them as root);
280
280
  - `sync.exit(status)`, which ends the process at once, as Node's `process.exit()` does: no `catch`, `finally`, microtask, timer or signal handler runs after it, because the call never returns. First the writes the program has made reach their descriptors, in order (a broken pipe drops the rest for that descriptor), and terminal modes the program set with `tty` are put back. Then the process ends the way any exit does: its descriptors are closed and written back, its parent gets `SIGCHLD`, and `wait` reports `status`. Children keep running. A JS program has no threads of its own (`Worker` is withheld), so nothing of it outlives the call. `exit(status)` (below) unwinds instead, so the program's `catch` and `finally` blocks run;
281
- - `spawn({ argv, env, cwd, pgid, session, stdin, stdout, stderr })` starts a command (found as `kernel.run` finds it) as a child process and resolves with `{ pid, stdin, stdout, stderr, wait(), kill(signal) }`. Each stdio is `'inherit'` (the default: the program's own), `'pipe'` (a web stream on a kernel pipe), `'null'`, or one of the program's descriptors. `env` is the child's whole environment (the program's by default), and `cwd` defaults to the program's. The child is in the program's process group and session, as after `fork` (Node's `detached: false`), so the terminal's `^C` and `^Z` reach it along with the program. `pgid: 0` makes it lead a new group in the program's session, set before it runs, so a `kill(-pid)` right after the call reaches it and everything it starts (`npx`, then `node`); on the program's terminal that group is in the background, so a read from the terminal stops it with `SIGTTIN` (`EIO` when it ignores `SIGTTIN`), as on Linux. `pgid: N` joins group N, which must be in the program's session: `EPERM` for another session's group, `ESRCH` when there is none. `session: true` makes it lead a new session and a new group (`setsid`, what Node's `detached: true` does on POSIX): its pid is its `pgid` and its `sid`, it has no controlling terminal (`/dev/tty` is `ENXIO`) and the terminal's keys don't signal it. `session` together with `pgid`, or a `pgid` that isn't a non-negative integer, is `EINVAL`. A command that does not exist rejects with `ENOENT`;
281
+ - `spawn({ argv, argv0, env, cwd, pgid, session, stdin, stdout, stderr })` starts a command (found as `kernel.run` finds it) as a child process and resolves with `{ pid, stdin, stdout, stderr, wait(), kill(signal) }`. Each stdio is `'inherit'` (the default: the program's own), `'pipe'` (a web stream on a kernel pipe), `'null'`, or one of the program's descriptors. `env` is the child's whole environment (the program's by default), and `cwd` defaults to the program's. The command run is `argv[0]`; `argv0`, when given, is the `argv[0]` the child sees instead, as given (no basename; an empty string too), as with Node's `options.argv0`: `spawn({ argv: ['/bin/sh', '-c', 'echo $0'], argv0: '/bin/sh' })` prints `/bin/sh`, where without it the child sees the command's own name (`sh`). A command whose package manifest sets its `argv0` (a multi-call program's applets, such as coreutils') keeps that one. An `argv0` that isn't a string is `EINVAL`. The child is in the program's process group and session, as after `fork` (Node's `detached: false`), so the terminal's `^C` and `^Z` reach it along with the program. `pgid: 0` makes it lead a new group in the program's session, set before it runs, so a `kill(-pid)` right after the call reaches it and everything it starts (`npx`, then `node`); on the program's terminal that group is in the background, so a read from the terminal stops it with `SIGTTIN` (`EIO` when it ignores `SIGTTIN`), as on Linux. `pgid: N` joins group N, which must be in the program's session: `EPERM` for another session's group, `ESRCH` when there is none. `session: true` makes it lead a new session and a new group (`setsid`, what Node's `detached: true` does on POSIX): its pid is its `pgid` and its `sid`, it has no controlling terminal (`/dev/tty` is `ENXIO`) and the terminal's keys don't signal it. `session` together with `pgid`, or a `pgid` that isn't a non-negative integer, is `EINVAL`. A command that does not exist rejects with `ENOENT`;
282
282
  - `wait(pid)` resolves with `{ status }` once a child ends, or `{ status: 128 + signal, signal }` when a signal ended it, without blocking the program; `kill(pid, signal)` signals any process, by number or as `'SIGTERM'` (the default);
283
283
  - `sudo(args)`, what the [`sudo`](#sudo) command calls: `args` are its arguments after `sudo`, and it resolves with `{ pid }`, the command the kernel started as the program's child, to `wait` for; `{ run }`, an argv to run as the caller (with `users: 'off'`); `{ list }`, the text of `sudo -l`; or `{ error, errno }`, the message to print;
284
284
  - `net`: `listen({ port, host, backlog })` and `connect({ host, port, signal })` (`host` defaults to `127.0.0.1`) on the kernel's loopback, the same sockets WASIX programs use, so a JS server is reached by other processes, by the page as `<port>.kernel.localhost` (`loopbackFetch`, `dial`) and by attached clients. A listener has its `port` (as bound, so port `0` picks a free one), `accept({ signal })`, `close()` and `for await` over its connections. A connection has `local` and `remote` addresses, `readable` and `writable` web streams (closing the writable shuts down the sending side, so the peer reads the end), and `close()`. The loopback is IPv4 (`127.0.0.1` by default, or `0.0.0.0`), so an IPv6 host such as `::1` fails with `EADDRNOTAVAIL` for both `listen` and `connect`, and binding and connecting fail as they would for a WASIX program (`EADDRINUSE`, `ECONNREFUSED`, `EADDRNOTAVAIL`). A blocked accept, read or write waits without holding anything in the kernel, so aborting it or closing the socket leaves nothing behind. A write to a connection the peer closed fails with `EPIPE` and no `SIGPIPE`;
@@ -664,7 +664,7 @@ The kernel writes `/etc/passwd` and `/etc/group` when it starts (see [Users](#us
664
664
  OPFS stores names, bytes, sizes and modification times, nothing else. Everything POSIX needs on top of that lives in an IndexedDB sidecar, so it survives reloads and new kernels:
665
665
 
666
666
  - mode bits (`chmod`);
667
- - access and change times, and a modification time set explicitly with `utime` (it holds until the file is written again); a directory's times move whenever an entry is created, removed or renamed in it, as POSIX asks, and a time set explicitly after such a change keeps winning, also when the change was made in another kernel and its write lands later; an Emscripten program reads a directory's times again after any call that can wait on another process (a pipe or socket read, `wait`, `poll`, a sleep), so a program that waits for a child or a peer to fill a directory sees the new times (rsync's final directory times rely on it), while a walk without waits reads each directory once; `chown`/`fchown` leave the times alone (Emscripten stamps the setattr of a chown with the current time, which isn't taken as an mtime);
667
+ - access and change times, and a modification time set explicitly with `utime` (it holds until the file is written again: any write or truncate moves it to the time of the write, also in Chrome, whose OPFS doesn't move a file's `lastModified` for writes through a sync access handle, so the kernel records the write's time itself, and it reaches the metadata store up to 100 ms after the write, so a tab closed inside that window keeps the old mtime on disk; the time follows a rename and is dropped with the file); a directory's times move whenever an entry is created, removed or renamed in it, as POSIX asks, and a time set explicitly after such a change keeps winning, also when the change was made in another kernel and its write lands later; an Emscripten program reads a directory's times again after any call that can wait on another process (a pipe or socket read, `wait`, `poll`, a sleep), so a program that waits for a child or a peer to fill a directory sees the new times (rsync's final directory times rely on it), while a walk without waits reads each directory once; `chown`/`fchown` leave the times alone (Emscripten stamps the setattr of a chown with the current time, which isn't taken as an mtime);
668
668
  - inode numbers that stay stable across renames;
669
669
  - symbolic links (`symlink`, `readlink`, `lstat`, and following them in paths), which exist only in the sidecar, with their own times: `touch -h`, `lutimes` and `utimensat` with `AT_SYMLINK_NOFOLLOW` set the link's, never the target's. A link's own mode can't be changed, as on Linux: `chmod -h`, `lchmod` and `fchmodat` with `AT_SYMLINK_NOFOLLOW` fail with `EOPNOTSUPP` (which `chmod -h` and `tar` ignore for links) and never follow it, and `open` with `O_PATH | O_NOFOLLOW` gives a descriptor for the link itself.
670
670
 
@@ -783,6 +783,7 @@ function syncAccessHandles(scope = globalThis) {
783
783
  function isHandle(child) {
784
784
  return "kind" in child;
785
785
  }
786
+ var FILE_TIMES_MS = 100;
786
787
  var OpfsFs = class {
787
788
  root;
788
789
  meta;
@@ -1053,8 +1054,50 @@ var OpfsFs = class {
1053
1054
  const fallback = directory || EXECUTABLE.test(path) ? 493 : 420;
1054
1055
  return (directory ? S_IFDIR : S_IFREG) | (meta?.mode ?? fallback);
1055
1056
  }
1057
+ id = crypto.randomUUID();
1056
1058
  dirTimes = /* @__PURE__ */ new Map();
1057
1059
  dirDirty = /* @__PURE__ */ new Set();
1060
+ fileTimes = /* @__PURE__ */ new Map();
1061
+ written(target) {
1062
+ this.committed(target);
1063
+ if (!this.ranged) return;
1064
+ this.fileTimes.set(target, Date.now());
1065
+ this.scheduleFileTimes();
1066
+ }
1067
+ scheduleFileTimes() {
1068
+ if (this.flushing.file || this.fileTimes.size === 0) return;
1069
+ this.flushing.file = setTimeout(() => {
1070
+ void this.flushFileTimes().catch(() => void 0).finally(() => {
1071
+ this.flushing.file = void 0;
1072
+ this.scheduleFileTimes();
1073
+ });
1074
+ }, FILE_TIMES_MS);
1075
+ this.flushing.file.unref?.();
1076
+ }
1077
+ async flushFileTimes() {
1078
+ for (const [path, ms] of [...this.fileTimes]) {
1079
+ const mtimeFor = await this.modified(path).catch(() => 0);
1080
+ await this.meta.update(
1081
+ path,
1082
+ (entry) => this.fileTimes.get(path) !== ms || this.explicitWins(entry, ms) ? entry : { ...entry, path, mtimeMs: ms, mtimeFor, ctimeMs: ms }
1083
+ );
1084
+ if (this.fileTimes.get(path) === ms) this.fileTimes.delete(path);
1085
+ }
1086
+ }
1087
+ timesOf(path) {
1088
+ return this.dirTimes.get(path) ?? this.fileTimes.get(path);
1089
+ }
1090
+ explicitWins(entry, ms) {
1091
+ if (entry?.timesAt === void 0 || entry.timesBy === this.id) return false;
1092
+ return entry.timesAt >= ms;
1093
+ }
1094
+ movePending(from, to) {
1095
+ for (const [path, ms] of [...this.fileTimes]) {
1096
+ if (!within2(path, from)) continue;
1097
+ this.fileTimes.delete(path);
1098
+ if (to !== void 0) this.fileTimes.set(to + path.slice(from.length), ms);
1099
+ }
1100
+ }
1058
1101
  flushing = {};
1059
1102
  touch(...dirs) {
1060
1103
  const now = Date.now();
@@ -1078,11 +1121,10 @@ var OpfsFs = class {
1078
1121
  if (this.dirTimes.get(path) === ms) this.dirTimes.delete(path);
1079
1122
  }
1080
1123
  }
1081
- async statOf(path, handle, meta) {
1124
+ async statOf(path, handle, meta, touched) {
1082
1125
  const directory = handle.kind === "directory";
1083
1126
  const file = directory ? void 0 : await handle.getFile();
1084
1127
  const modified = file?.lastModified ?? 0;
1085
- const touched = directory ? this.dirTimes.get(path) : void 0;
1086
1128
  const kept = meta?.mtimeMs !== void 0 && meta.mtimeFor === modified ? meta.mtimeMs : modified;
1087
1129
  const mtime = touched ?? kept;
1088
1130
  return {
@@ -1114,13 +1156,14 @@ var OpfsFs = class {
1114
1156
  }
1115
1157
  async describe(found) {
1116
1158
  if (found.link) return this.owned(this.linkStat(found.link), found.path, found.link);
1159
+ const touched = this.timesOf(found.path);
1117
1160
  if (!this.owners) {
1118
1161
  const [meta2] = await this.meta.get([found.path]);
1119
- return this.statOf(found.path, found.handle, meta2);
1162
+ return this.statOf(found.path, found.handle, meta2, touched);
1120
1163
  }
1121
1164
  const entries = await this.meta.get(chain(found.path));
1122
1165
  const meta = entries.pop();
1123
- const stat = await this.statOf(found.path, found.handle, meta);
1166
+ const stat = await this.statOf(found.path, found.handle, meta, touched);
1124
1167
  return { ...stat, ...ownOf(meta) ?? inherit(entries) };
1125
1168
  }
1126
1169
  async stat(path) {
@@ -1177,13 +1220,14 @@ var OpfsFs = class {
1177
1220
  async pwrite(path, offset, bytes) {
1178
1221
  const [handle, , target] = await this.fileFor(path);
1179
1222
  await this.retrying(path, () => positioned(handle, (io) => io.write(offset, bytes)), true);
1180
- this.committed(target);
1223
+ this.written(target);
1181
1224
  }
1182
1225
  async truncate(path, size) {
1183
1226
  const handle = await this.existing(path, true);
1184
- const target = this.remote?.watched() ? (await this.locate(path, true)).path : path;
1227
+ const resolve = this.ranged || this.remote?.watched();
1228
+ const target = resolve ? (await this.locate(path, true)).path : path;
1185
1229
  await this.retrying(path, () => positioned(handle, (io) => io.truncate(size)), true);
1186
- this.committed(target);
1230
+ this.written(target);
1187
1231
  }
1188
1232
  async writeSession(path, truncate) {
1189
1233
  const [handle, , target] = await this.fileFor(path);
@@ -1273,13 +1317,16 @@ var OpfsFs = class {
1273
1317
  async readdirStat(path) {
1274
1318
  const [dir, children2] = await this.listing(path);
1275
1319
  const base = dir === "/" ? "" : dir;
1320
+ const touched = children2.map(([name2]) => this.timesOf(`${base}/${name2}`));
1276
1321
  const metas = await this.meta.get(children2.map(([name2]) => `${base}/${name2}`));
1277
1322
  const handed2 = this.owners ? await this.handedTo(dir) : void 0;
1278
1323
  const owned4 = (stat, entry) => handed2 ? { ...stat, ...ownOf(entry) ?? handed2 } : stat;
1279
1324
  return Promise.all(
1280
1325
  children2.map(async ([name2, child], i) => {
1281
1326
  if (!isHandle(child)) return [name2, owned4(this.linkStat(child), child)];
1282
- const stat = await this.statOf(`${base}/${name2}`, child, metas[i]).catch(() => null);
1327
+ const stat = await this.statOf(`${base}/${name2}`, child, metas[i], touched[i]).catch(
1328
+ () => null
1329
+ );
1283
1330
  return [name2, stat && owned4(stat, metas[i])];
1284
1331
  })
1285
1332
  );
@@ -1337,6 +1384,7 @@ var OpfsFs = class {
1337
1384
  throw translate(err, path);
1338
1385
  }
1339
1386
  this.changed(found.path);
1387
+ this.movePending(found.path);
1340
1388
  }
1341
1389
  await this.meta.remove([found.path]);
1342
1390
  this.touch(parentOf3(found.path));
@@ -1400,6 +1448,7 @@ var OpfsFs = class {
1400
1448
  const moved2 = await this.place(source.handle, dir, staging);
1401
1449
  if (swap) await this.rm(destination, { recursive: true });
1402
1450
  await this.meta.move(source.path, destination, carry);
1451
+ this.movePending(source.path, destination);
1403
1452
  if (!moved2) await this.rm(source.path, { recursive: true });
1404
1453
  let copied = !moved2;
1405
1454
  if (swap) {
@@ -1521,6 +1570,7 @@ var OpfsFs = class {
1521
1570
  const handle = found.handle;
1522
1571
  const file = handle.kind === "file" ? await handle.getFile() : void 0;
1523
1572
  this.dirTimes.delete(found.path);
1573
+ this.fileTimes.delete(found.path);
1524
1574
  const ctimeMs = Date.now();
1525
1575
  await this.meta.update(found.path, (entry) => ({
1526
1576
  ...entry,
@@ -1529,7 +1579,8 @@ var OpfsFs = class {
1529
1579
  mtimeMs: mtime.getTime(),
1530
1580
  mtimeFor: file?.lastModified ?? 0,
1531
1581
  ctimeMs,
1532
- timesAt: ctimeMs
1582
+ timesAt: ctimeMs,
1583
+ timesBy: this.id
1533
1584
  }));
1534
1585
  }
1535
1586
  async reconcile() {
@@ -6820,7 +6871,8 @@ function commandOf(pkg, name2, raw, packageAbi, shared) {
6820
6871
  return { name: name2, abi: packageAbi, glue: script, wasm: script, argv0: name2, script, ...withEnv };
6821
6872
  }
6822
6873
  const abi = abiOf(raw.abi, packageAbi);
6823
- const argv0 = typeof raw.argv0 === "string" && raw.argv0 ? raw.argv0 : name2;
6874
+ const fixed = typeof raw.argv0 === "string" && raw.argv0 !== "";
6875
+ const argv0 = fixed ? raw.argv0 : name2;
6824
6876
  const args = argsOf(pkg, raw.args);
6825
6877
  if (abi === "js") {
6826
6878
  const module = inside(pkg, raw.module);
@@ -6831,6 +6883,7 @@ function commandOf(pkg, name2, raw, packageAbi, shared) {
6831
6883
  glue: module,
6832
6884
  wasm: module,
6833
6885
  argv0,
6886
+ ...fixed ? { argv0Fixed: true } : {},
6834
6887
  ...raw.argv0Path === true ? { argv0Path: true } : {},
6835
6888
  ...args ? { args } : {},
6836
6889
  ...withEnv
@@ -6846,6 +6899,7 @@ function commandOf(pkg, name2, raw, packageAbi, shared) {
6846
6899
  glue,
6847
6900
  wasm,
6848
6901
  argv0,
6902
+ ...fixed ? { argv0Fixed: true } : {},
6849
6903
  ...raw.argv0Path === true ? { argv0Path: true } : {},
6850
6904
  ...abi === "wasi" && raw.preopenRoot === true ? { preopenRoot: true } : {},
6851
6905
  ...args ? { args } : {},
@@ -11378,6 +11432,7 @@ async function runChild(req, deps) {
11378
11432
  {
11379
11433
  file: req.file,
11380
11434
  argv: req.argv,
11435
+ ...req.argv0 !== void 0 ? { argv0: req.argv0 } : {},
11381
11436
  env: req.env,
11382
11437
  cwd: req.cwd,
11383
11438
  ...req.pgid !== void 0 ? { pgid: req.pgid } : {},
@@ -12336,9 +12391,10 @@ var WasmProcess = class {
12336
12391
  return "deliver";
12337
12392
  }
12338
12393
  async spawn(req) {
12339
- const { file, argv, env, cwd, stdio, inherit: inherit2, exec, pgid, session } = req;
12394
+ const { file, argv, argv0, env, cwd, stdio, inherit: inherit2, exec, pgid, session } = req;
12340
12395
  const group = { ...pgid !== void 0 ? { pgid } : {}, ...session ? { session } : {} };
12341
- const spawned = { file, argv, env, cwd, ...group, ...exec ? { exec } : {} };
12396
+ const named = argv0 !== void 0 ? { argv0 } : {};
12397
+ const spawned = { file, argv, ...named, env, cwd, ...group, ...exec ? { exec } : {} };
12342
12398
  const pid = await this.children.spawn(spawned, stdio, inherit2);
12343
12399
  if (!exec) return pid;
12344
12400
  if (this.killed) void Promise.allSettled([this.options.kill?.(pid, SIG.KILL)]);
@@ -16166,6 +16222,7 @@ function targetOf(command) {
16166
16222
  glue: command.glue,
16167
16223
  wasm: command.wasm,
16168
16224
  argv0: command.argv0,
16225
+ ...command.argv0Fixed ? { fixed: true } : {},
16169
16226
  ...command.args ? { prefix: command.args } : {},
16170
16227
  ...command.env ? { env: command.env } : {},
16171
16228
  ...command.unset ? { unset: command.unset } : {},
@@ -16528,8 +16585,10 @@ var Launcher = class {
16528
16585
  const target = withScriptEnv(found, command);
16529
16586
  return { target, args: [...target.prefix ?? [], ...passed], via: [interp] };
16530
16587
  }
16531
- async plan(file, argv, cwd, scope) {
16532
- const direct = await this.resolve(file, argv[0] ?? file, cwd, scope);
16588
+ async plan(file, argv, cwd, scope, argv0) {
16589
+ const found = await this.resolve(file, argv[0] ?? file, cwd, scope);
16590
+ const named = typeof argv0 === "string" && found && !found.fixed;
16591
+ const direct = named ? { ...found, argv0 } : found;
16533
16592
  if (direct) return { target: direct, args: [...direct.prefix ?? [], ...argv.slice(1)] };
16534
16593
  const script = await this.interpreted(file, argv, cwd, scope);
16535
16594
  return script && { ...script, script: true };
@@ -16918,7 +16977,7 @@ var Launcher = class {
16918
16977
  }
16919
16978
  const cred = grant ? grant.cred : this.credOf(ppid);
16920
16979
  const scope = grant ? "" : await this.scopeOf(cred.euid);
16921
- const planned = await this.plan(req.file, req.argv, req.cwd, scope);
16980
+ const planned = await this.plan(req.file, req.argv, req.cwd, scope, req.argv0);
16922
16981
  try {
16923
16982
  await this.mayExec(cred, { ...req, pid: ppid }, planned);
16924
16983
  } catch (err) {