warpmetal 0.8.10 → 0.8.12

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
@@ -317,9 +317,10 @@ compatible external signer.
317
317
  Wallet key management and signing remain outside this package.
318
318
  - Destructive or state-changing commands require explicit confirmations and
319
319
  generate idempotency keys by default.
320
- - Runtime bootstrap credentials remain memory-only and are requested only after
321
- the first host key has been pinned and strictly reverified. Signed supervisor
322
- bundles are checksum- and signature-verified before OpenSSH uploads them.
320
+ - Runtime bootstrap credentials remain memory-only. Manual installation asks
321
+ for one only after the first host key has been pinned and strictly reverified;
322
+ automatic reload bootstrap is rendered directly into provider-bound
323
+ cloud-init. Signed supervisor bundles are checksum- and signature-verified.
323
324
  - Each agent gets a distinct SSH key forced into exactly one sandbox. Token-free
324
325
  connection profiles pin the VPS host key and contain no owner credential or
325
326
  private-key material.
@@ -332,11 +333,30 @@ compatible external signer.
332
333
  external workspace, lifetime, and start time. The wait completes only when
333
334
  the observed digest and generation both match the accepted target.
334
335
  - Guarded reload powers the server off first. Runtime-enabled reload requires a
335
- second acknowledgment, after which the CLI guides supervisor reinstall and
336
- pinned connection-profile refresh. Only a locally recorded reload operation
337
- that succeeds and reports an owner-host-key refresh opens one new managed
338
- trust epoch; failed or ambiguous reloads retain the old pin and never permit
339
- replacement. Erased workspaces are never described as recoverable.
336
+ second acknowledgment because all sandbox workspaces are erased. WarpMetal
337
+ places the approved signed Runtime bootstrap in reload cloud-init
338
+ automatically; wait for Runtime readiness, then refresh pinned sandbox
339
+ connection profiles. Only a locally recorded reload operation that succeeds
340
+ and reports an owner-host-key refresh opens one new managed trust epoch;
341
+ failed or ambiguous reloads retain the old pin and never permit replacement.
342
+ Erased workspaces are never described as recoverable.
343
+
344
+ For a Runtime-enabled reload, no separate installation command is part of the
345
+ successful path:
346
+
347
+ ```sh
348
+ warpmetal server reload \
349
+ --server <serverId> --confirm ERASE --power-off-first \
350
+ --acknowledge-agent-runtime-reset --wait --json
351
+ warpmetal runtime get --server <serverId> --wait --json
352
+ warpmetal sandbox access refresh \
353
+ --server <serverId> --sandbox <sandboxId> --grant <grantId> \
354
+ --connection-file <profile-path> --confirm REFRESH --wait --json
355
+ ```
356
+
357
+ The successful reload records a new operation-bound owner SSH trust epoch.
358
+ Verify the replacement host key before using owner SSH; sandbox access remains
359
+ strictly pinned through each refreshed connection profile.
340
360
 
341
361
  ## Agent Runtime example
342
362
 
@@ -386,10 +406,12 @@ ssh -t <alias> claude
386
406
  ssh <alias> claude -p '<task>'
387
407
  ssh -t <alias> agent
388
408
  ssh <alias> agent -p '<task>'
409
+ ssh -t <alias> gemini
410
+ ssh <alias> gemini -p '<task>'
389
411
  ```
390
412
 
391
- Install and authenticate Codex, Claude Code, or Cursor CLI inside the
392
- sandbox first. Provider credentials are sandbox-owned and remain in its
413
+ Install and authenticate Codex, Claude Code, Cursor CLI, or Gemini CLI inside
414
+ the sandbox first. Provider credentials are sandbox-owned and remain in its
393
415
  persistent home; WarpMetal does not install these tools, perform their login,
394
416
  or receive their credentials.
395
417
 
@@ -434,6 +456,14 @@ weaken sandbox forwarding controls to make the IDE connect. Use the supported
434
456
  [headless mode](https://cursor.com/docs/cli/headless) with
435
457
  `ssh <alias> agent -p '<task>'` instead.
436
458
 
459
+ Use the official Gemini CLI [installation guide](https://geminicli.com/docs/get-started/installation/)
460
+ before running `ssh -t <alias> gemini`, or use its documented
461
+ [headless mode](https://geminicli.com/docs/cli/headless/) with
462
+ `ssh <alias> gemini -p '<task>'`. Gemini's optional Docker or Podman sandbox is
463
+ normally unavailable inside the WarpMetal sandbox because no host
464
+ container-engine socket is exposed; run Gemini directly inside the existing
465
+ outer sandbox and choose its approvals yourself.
466
+
437
467
  On the first install in a server trust epoch, JSON output includes
438
468
  `hostKeyTrust.state: "trusted_first_use"` and the safe Ed25519 fingerprint.
439
469
  Later installs report `"matched"`. First-use trust protects continuity after
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "warpmetal",
3
- "version": "0.8.10",
3
+ "version": "0.8.12",
4
4
  "description": "Agent-safe CLI and skill for purchasing, renewing, and managing WarpMetal VPS servers",
5
5
  "type": "module",
6
6
  "bin": {
@@ -254,8 +254,8 @@ warpmetal server power \
254
254
  For a destructive reload, explain that every server-disk file is erased and
255
255
  obtain explicit approval. If Agent Runtime is enabled, also explain that every
256
256
  sandbox workspace is lost, desired sandboxes return as empty workspaces after
257
- reinstall, and pinned profiles must be refreshed. Then use only the guarded
258
- command:
257
+ automatic Runtime setup, and pinned profiles must be refreshed. Then use only
258
+ the guarded command:
259
259
 
260
260
  ```sh
261
261
  warpmetal server reload \
@@ -271,14 +271,18 @@ warpmetal server reload \
271
271
  The CLI requires the recovery owner credential so it can keep polling after
272
272
  reload revokes SSH-derived access tokens. CLI 0.8.8 records a new SSH trust
273
273
  epoch only when that exact local reload operation succeeds and reports that the
274
- owner host key needs refresh. The next Runtime install may trust the first
275
- observed Ed25519 host key once in that epoch, then must reconnect strictly
276
- before requesting bootstrap. Failed or ambiguous reloads retain the prior pin;
274
+ owner host key needs refresh. Failed or ambiguous reloads retain the prior pin;
277
275
  never bypass a mismatch or delete a pin to force a retry. A provider-console
278
- pre-seed is the optional higher-assurance path. Then wait for grants to become applied and refresh
279
- every connection profile with `sandbox access refresh --confirm REFRESH`
280
- before connecting. Do not fall back to raw API calls for deletion, networking,
281
- or another unsupported mutation.
276
+ pre-seed is the optional higher-assurance path.
277
+
278
+ After a successful Runtime-enabled reload, WarpMetal performs signed Runtime
279
+ setup automatically. Do not run a separate installation command. Wait with
280
+ `warpmetal runtime get --server <serverId> --wait --json`, then wait for grants
281
+ to become `applied` and refresh every connection profile with
282
+ `sandbox access refresh --confirm REFRESH` before connecting. The successful
283
+ operation created a new owner host-trust epoch, so verify the replacement host
284
+ key before owner SSH. Do not fall back to raw API calls for deletion,
285
+ networking, or another unsupported mutation.
282
286
 
283
287
  ## Use Agent Runtime
284
288
 
@@ -286,12 +290,13 @@ Agent Runtime is optional and shares one owner's VPS only among that owner's
286
290
  agents. Discover live `agentRuntime` capacity and OS support before choosing
287
291
  sizes. Use `--runtime-file` to include sandbox intent in an unpaid order, or
288
292
  `warpmetal runtime enable` after the VPS is ready. Supervisor installation is
289
- separate and requires approval plus `--confirm INSTALL`.
293
+ separate and requires approval plus `--confirm INSTALL` for initial setup or
294
+ explicit repair; Runtime-enabled OS reloads use automatic signed cloud-init.
290
295
 
291
296
  With CLI 0.8.8, that confirmation also authorizes managed trust on first use
292
297
  when the exact server trust epoch has no pin. The CLI performs only an owner-
293
298
  key-authenticated `ssh true`, pins the first observed Ed25519 key, immediately
294
- reconnects strictly, and requests bootstrap only afterward. Later connections
299
+ reconnects strictly before requesting bootstrap. Later connections
295
300
  must match. Explain that TOFU cannot detect an active attacker on the first
296
301
  connection; never use `ssh-keyscan`, accept a mismatch, or expose a generic pin
297
302
  reset.
@@ -322,8 +327,10 @@ ssh <alias>
322
327
 
323
328
  The local alias preserves the forced gateway: it cannot open a host shell and
324
329
  does not relax forwarding denial. Provider authentication and credentials stay
325
- inside the sandbox. After an authenticated profile refresh, update the alias
326
- only with the explicit second confirmation:
330
+ inside the sandbox. Install and authenticate Codex, Claude Code, Cursor CLI, or
331
+ Gemini CLI inside the sandbox; use the entry points and compatibility guidance
332
+ in [references/runtime.md](references/runtime.md). After an authenticated
333
+ profile refresh, update the alias only with the explicit second confirmation:
327
334
 
328
335
  ```sh
329
336
  warpmetal sandbox access refresh \
@@ -201,7 +201,18 @@ Reload requires the recovery owner credential rather than a short-lived
201
201
  SSH-derived token. `--power-off-first` authorizes shutdown and powered-off
202
202
  verification inside the same operation. When Agent Runtime is enabled,
203
203
  `--acknowledge-agent-runtime-reset` is required because workspaces are erased,
204
- the supervisor must be reinstalled, and connection profiles must be refreshed.
204
+ the Runtime identity is replaced, empty sandboxes are reconciled automatically,
205
+ and connection profiles must be refreshed. After a successful reload, wait for
206
+ automatic setup before refreshing profiles:
207
+
208
+ ```sh
209
+ warpmetal runtime get --server <serverId> --wait --json
210
+ ```
211
+
212
+ The successful operation records a new owner SSH trust epoch. Verify the
213
+ replacement host key before owner SSH. Manual Runtime installation remains an
214
+ explicit repair path when automatic setup fails; it is not part of a
215
+ successful reload.
205
216
 
206
217
  Use `--token-file` only for recovery when local state is unavailable. Prefer
207
218
  `WARPMETAL_OWNER_TOKEN` or `WARPMETAL_ACCESS_TOKEN` for a single command over a
@@ -277,7 +288,7 @@ returns the OpenSSH or remote exit status. `--connection-file` on grant
277
288
  creation requires `--wait`.
278
289
  `sandbox access refresh` atomically replaces a stale token-free profile with
279
290
  the currently applied grant and API-reported pinned host keys; use it after an
280
- OS reload and supervisor reinstall.
291
+ OS reload once automatic Runtime reconciliation is ready.
281
292
 
282
293
  `sandbox access install-ssh` is local-only and requires CLI 0.8.10 or newer.
283
294
  It turns the reviewed profile and sandbox-private identity into a concrete
@@ -300,8 +311,14 @@ ssh -t <alias> claude
300
311
  ssh <alias> claude -p '<task>'
301
312
  ssh -t <alias> agent
302
313
  ssh <alias> agent -p '<task>'
314
+ ssh -t <alias> gemini
315
+ ssh <alias> gemini -p '<task>'
303
316
  ```
304
317
 
318
+ Install and authenticate Codex, Claude Code, Cursor CLI, or Gemini CLI inside
319
+ the sandbox first. WarpMetal does not install, authenticate, configure, or
320
+ receive credentials for those tools.
321
+
305
322
  [Codex Desktop](https://learn.chatgpt.com/docs/remote-connections) reads
306
323
  the concrete alias from `~/.ssh/config` and starts Codex through the sandbox
307
324
  login shell, so Codex must be installed and on that login-shell `PATH`. The
@@ -310,6 +327,13 @@ not compatible with this boundary. Keep forwarding denied and use the
310
327
  [Cursor CLI](https://cursor.com/docs/cli/overview) interactive or
311
328
  [headless](https://cursor.com/docs/cli/headless) commands shown above.
312
329
 
330
+ Install Gemini CLI from its official [installation guide](https://geminicli.com/docs/get-started/installation/)
331
+ and use its documented [headless mode](https://geminicli.com/docs/cli/headless/)
332
+ for `gemini -p`. Gemini's optional Docker or Podman sandbox is normally
333
+ unavailable inside the WarpMetal sandbox because no host container-engine
334
+ socket is exposed; run Gemini directly inside the existing outer sandbox and
335
+ choose its approvals yourself.
336
+
313
337
  ## Skill installation and state
314
338
 
315
339
  ```sh
@@ -208,11 +208,12 @@ The token-free profile is written only after the grant is `applied` and the
208
208
  API supplies verified VPS host keys. Do not print or open that profile in an
209
209
  agent conversation.
210
210
 
211
- After a destructive OS reload, reinstall the supervisor and wait for the
212
- retained active grant to become `applied`, then replace its stale pinned
213
- profile:
211
+ After a successful destructive OS reload, the signed Runtime bootstrap runs
212
+ automatically. Wait for Runtime readiness and for the retained active grant to
213
+ become `applied`, then replace its stale pinned profile:
214
214
 
215
215
  ```sh
216
+ warpmetal runtime get --server <serverId> --wait --json
216
217
  warpmetal sandbox access refresh \
217
218
  --server <serverId> \
218
219
  --sandbox <sandboxId> \
@@ -225,7 +226,8 @@ warpmetal sandbox access refresh \
225
226
 
226
227
  The sandbox record is retained, but its old workspace is not; reconciliation
227
228
  creates a new empty workspace. Never bypass a host-key mismatch or reuse the
228
- pre-reload profile.
229
+ pre-reload profile. Use manual Runtime installation only as an explicitly
230
+ approved repair when the backend reports that automatic setup failed.
229
231
 
230
232
  ### Install a concrete OpenSSH alias
231
233
 
@@ -294,11 +296,14 @@ ssh -t <alias> claude
294
296
  ssh <alias> claude -p '<task>'
295
297
  ssh -t <alias> agent
296
298
  ssh <alias> agent -p '<task>'
299
+ ssh -t <alias> gemini
300
+ ssh <alias> gemini -p '<task>'
297
301
  ```
298
302
 
299
- Provider authentication and credentials are sandbox-owned and persist only in
300
- the sandbox home. WarpMetal does not install, authenticate, configure, or
301
- receive credentials for Codex, Claude Code, or Cursor CLI.
303
+ Install and authenticate Codex, Claude Code, Cursor CLI, or Gemini CLI inside
304
+ the sandbox first. Provider authentication and credentials are sandbox-owned
305
+ and persist only in the sandbox home. WarpMetal does not install, authenticate,
306
+ configure, or receive credentials for those tools.
302
307
 
303
308
  [Codex Desktop](https://learn.chatgpt.com/docs/remote-connections)
304
309
  discovers a concrete alias through `~/.ssh/config`, requires ordinary
@@ -314,6 +319,14 @@ restricted alias. Do not relax forwarding controls. Use the official
314
319
  [headless mode](https://cursor.com/docs/cli/headless) with
315
320
  `ssh <alias> agent -p '<task>'` instead.
316
321
 
322
+ Follow Gemini CLI's official [installation guide](https://geminicli.com/docs/get-started/installation/),
323
+ then use `ssh -t <alias> gemini` interactively or its documented
324
+ [headless mode](https://geminicli.com/docs/cli/headless/) with
325
+ `ssh <alias> gemini -p '<task>'`. Gemini's optional Docker or Podman sandbox is
326
+ normally unavailable inside the WarpMetal sandbox because no host
327
+ container-engine socket is exposed; run Gemini directly inside the existing
328
+ outer sandbox and choose its approvals yourself.
329
+
317
330
  Connect without an owner management credential:
318
331
 
319
332
  ```sh
@@ -95,9 +95,10 @@ A reload requires both `confirm: "ERASE"` and `powerOffFirst: true`. Treat
95
95
  server, wait until it is powered off, and then erase and reinstall it inside
96
96
  one lifecycle operation. When Agent Runtime is enabled, also require
97
97
  `acknowledgeAgentRuntimeReset`: all sandbox workspaces are permanently erased,
98
- the supervisor identity is revoked, desired sandboxes are recreated empty
99
- after reinstall, and all pinned profiles require refresh. Use only the guarded
100
- CLI command and stop on `manual_review`. Reload invalidates the prior
98
+ the supervisor identity is revoked, signed Runtime setup runs automatically,
99
+ desired sandboxes are recreated empty, and all pinned profiles require refresh.
100
+ Use only the guarded CLI command, wait for Runtime readiness, and stop on
101
+ `manual_review`. Reload invalidates the prior
101
102
  owner-facing SSH host-key trust decision; the provider may rotate or preserve
102
103
  the key. CLI 0.8.8 opens one new managed trust epoch only after the exact local
103
104
  reload operation succeeds and reports that refresh is required. The first
package/src/cli.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
2
  import { readFile, stat } from "node:fs/promises";
3
+ import { homedir } from "node:os";
3
4
  import { basename, join, resolve } from "node:path";
4
5
 
5
6
  import {
@@ -147,6 +148,14 @@ SSH host trust (CLI 0.8.8+):
147
148
  active attacker on the first connection. Provider-console pre-enrollment is
148
149
  the optional higher-assurance alternative.
149
150
 
151
+ Runtime-enabled reloads:
152
+ Reload still requires --acknowledge-agent-runtime-reset because every
153
+ sandbox workspace is erased. After a successful reload, Runtime setup is
154
+ automatic: use warpmetal runtime get --server <serverId> --wait --json,
155
+ then refresh every sandbox connection profile before reconnecting. The
156
+ successful operation also records a new owner SSH trust epoch; verify the
157
+ replacement host key before owner SSH.
158
+
150
159
  Sandbox SSH aliases:
151
160
  Refresh the authenticated connection profile before refreshing an existing
152
161
  alias:
@@ -165,6 +174,12 @@ Sandbox SSH aliases:
165
174
  ssh <alias> claude -p "<prompt>"
166
175
  ssh -t <alias> agent
167
176
  ssh <alias> agent -p "<prompt>"
177
+ ssh -t <alias> gemini
178
+ ssh <alias> gemini -p "<prompt>"
179
+
180
+ Install and authenticate Codex, Claude Code, Cursor CLI, or Gemini CLI inside
181
+ the sandbox first. WarpMetal does not install, authenticate, configure, or
182
+ receive credentials for those tools.
168
183
 
169
184
  Codex Desktop discovers the concrete alias through ~/.ssh/config and opens
170
185
  the sandbox login shell, so Codex must be available on the login-shell PATH.
@@ -172,10 +187,17 @@ Sandbox SSH aliases:
172
187
  a host shell, and do not relax isolation: ClearAllForwardings yes keeps all
173
188
  forwarding disabled.
174
189
 
190
+ Gemini's optional Docker or Podman sandbox is normally unavailable inside
191
+ the WarpMetal sandbox because no host container-engine socket is exposed;
192
+ run Gemini directly inside the existing outer sandbox and choose its
193
+ approvals yourself.
194
+
175
195
  Compatibility references:
176
196
  https://learn.chatgpt.com/docs/remote-connections
177
197
  https://cursor.com/docs/cli/overview
178
198
  https://cursor.com/docs/cli/headless
199
+ https://geminicli.com/docs/get-started/installation/
200
+ https://geminicli.com/docs/cli/headless/
179
201
 
180
202
  Credential environment variables:
181
203
  WARPMETAL_OWNER_TOKEN Recovery/bootstrap credential for one explicit command
@@ -1982,7 +2004,7 @@ async function applyReloadResult(
1982
2004
  await store.invalidateServerAccess(serverId);
1983
2005
  if (operation.result?.reloadImpact?.agentRuntimeAffected) {
1984
2006
  await store.saveRuntime(serverId, {
1985
- state: "needs_reinstall",
2007
+ state: "pending_install",
1986
2008
  desiredRevision: undefined,
1987
2009
  appliedRevision: 0,
1988
2010
  lastSeenAt: null,
@@ -2090,8 +2112,8 @@ async function handleServerReload(client, store, options, context) {
2090
2112
  (operation?.state === "succeeded" &&
2091
2113
  operation.result?.reloadImpact?.ownerKnownHostsNeedRefresh
2092
2114
  ? operation.result?.reloadImpact?.agentRuntimeAffected
2093
- ? " The next Runtime install will establish the operation-bound owner SSH trust epoch, then reinstall Agent Runtime and refresh every sandbox connection profile."
2094
- : " The next Runtime install will establish the operation-bound owner SSH trust epoch before reconnecting."
2115
+ ? " Agent Runtime setup is automatic. Wait for Agent Runtime to become ready, then refresh every sandbox connection profile. Verify the replacement host key before owner SSH."
2116
+ : " Verify the replacement host key before owner SSH; the successful reload recorded a new operation-bound trust epoch."
2095
2117
  : operation?.state === "succeeded"
2096
2118
  ? " WarpMetal did not report a host-key refresh; the existing managed pin remains required."
2097
2119
  : ""),
@@ -2740,7 +2762,7 @@ async function handleAccessInstallSsh(options, context) {
2740
2762
  alias: stringOption(options, "alias", { required: true }),
2741
2763
  connectionFile: stringOption(options, "connection-file", { required: true }),
2742
2764
  identity: stringOption(options, "identity", { required: true }),
2743
- homeDirectory: context.env.HOME,
2765
+ homeDirectory: context.env.HOME || context.env.USERPROFILE || homedir(),
2744
2766
  confirm: stringOption(options, "confirm"),
2745
2767
  });
2746
2768
  emit(
@@ -2755,7 +2777,7 @@ async function handleAccessInstallSsh(options, context) {
2755
2777
  async function handleAccessRemoveSsh(options, context) {
2756
2778
  const result = await removeSshAlias({
2757
2779
  alias: stringOption(options, "alias", { required: true }),
2758
- homeDirectory: context.env.HOME,
2780
+ homeDirectory: context.env.HOME || context.env.USERPROFILE || homedir(),
2759
2781
  confirm: stringOption(options, "confirm", { required: true }),
2760
2782
  });
2761
2783
  emit(
package/src/ssh-alias.js CHANGED
@@ -10,7 +10,7 @@ import {
10
10
  rmdir,
11
11
  unlink,
12
12
  } from "node:fs/promises";
13
- import { isAbsolute, join, parse, resolve } from "node:path";
13
+ import { isAbsolute, join, parse, posix, resolve, win32 } from "node:path";
14
14
  import { randomUUID } from "node:crypto";
15
15
 
16
16
  import {
@@ -21,7 +21,8 @@ import {
21
21
  import { CliError } from "./errors.js";
22
22
 
23
23
  const ALIAS_PATTERN = /^(?=.{1,63}$)[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
24
- const UNSAFE_PATH = /[\0\r\n\t%"'`$!&;<>|*?()[\]{}\\#]/;
24
+ const POSIX_UNSAFE_PATH = /[\0\r\n\t%"'`$!&;<>|*?()[\]{}\\#]/;
25
+ const WINDOWS_UNSAFE_PATH = /[\0\r\n\t%"'`$!&;<>|*?()[\]{}#]/;
25
26
  const MANAGED_HEADER = "# Managed by WarpMetal. Do not edit.";
26
27
 
27
28
  function invalid(message) {
@@ -37,17 +38,18 @@ export function validateSshAlias(value) {
37
38
  return value;
38
39
  }
39
40
 
40
- function validateAbsolutePath(value, label) {
41
- if (
42
- typeof value !== "string" ||
43
- !isAbsolute(value) ||
44
- resolve(value) !== value ||
45
- value === parse(value).root ||
46
- UNSAFE_PATH.test(value)
47
- ) {
41
+ export function validateAbsolutePath(value, label, platform = process.platform) {
42
+ const path = platform === "win32" ? win32 : posix;
43
+ if (typeof value !== "string" || !path.isAbsolute(value)) {
48
44
  invalid(`${label} must be an absolute path without unsafe interpolation characters.`);
49
45
  }
50
- return value;
46
+ const resolved = path.resolve(value);
47
+ const unsafePath =
48
+ platform === "win32" ? WINDOWS_UNSAFE_PATH : POSIX_UNSAFE_PATH;
49
+ if (resolved === path.parse(resolved).root || unsafePath.test(resolved)) {
50
+ invalid(`${label} must be an absolute path without unsafe interpolation characters.`);
51
+ }
52
+ return resolved;
51
53
  }
52
54
 
53
55
  function requireOwner(metadata, label) {
@@ -420,12 +422,19 @@ async function syncDirectory(path) {
420
422
  handle = await open(path, "r");
421
423
  await handle.sync();
422
424
  } catch (error) {
423
- if (!["EINVAL", "ENOTSUP", "EISDIR"].includes(error?.code)) throw error;
425
+ if (!isIgnorableDirectorySyncError(error)) throw error;
424
426
  } finally {
425
427
  await handle?.close().catch(() => {});
426
428
  }
427
429
  }
428
430
 
431
+ export function isIgnorableDirectorySyncError(error, platform = process.platform) {
432
+ return (
433
+ ["EINVAL", "ENOTSUP", "EISDIR"].includes(error?.code) ||
434
+ (platform === "win32" && ["EPERM", "EACCES"].includes(error?.code))
435
+ );
436
+ }
437
+
429
438
  async function commitStaged(staged) {
430
439
  try {
431
440
  for (const item of staged) {