@nimbus-sh/worker 0.3.0 → 0.5.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.
Files changed (136) hide show
  1. package/README.md +46 -43
  2. package/dist/_shared/preview-host.d.ts +14 -1
  3. package/dist/_shared/preview-host.d.ts.map +1 -1
  4. package/dist/_shared/preview-host.js +28 -3
  5. package/dist/_shared/session-router.d.ts +43 -0
  6. package/dist/_shared/session-router.d.ts.map +1 -1
  7. package/dist/_shared/session-router.js +59 -12
  8. package/dist/esbuild-wasm-bundle.generated.d.ts +1 -1
  9. package/dist/esbuild-wasm-bundle.generated.js +1 -1
  10. package/dist/facets/cirrus-real.js +1 -1
  11. package/dist/facets/durable-images.d.ts +50 -0
  12. package/dist/facets/durable-images.d.ts.map +1 -0
  13. package/dist/facets/durable-images.js +75 -0
  14. package/dist/facets/durable-slots.d.ts +45 -0
  15. package/dist/facets/durable-slots.d.ts.map +1 -0
  16. package/dist/facets/durable-slots.js +94 -0
  17. package/dist/facets/esbuild-transform.d.ts +10 -0
  18. package/dist/facets/esbuild-transform.d.ts.map +1 -0
  19. package/dist/facets/esbuild-transform.js +36 -0
  20. package/dist/facets/manager.d.ts +167 -12
  21. package/dist/facets/manager.d.ts.map +1 -1
  22. package/dist/facets/manager.js +575 -113
  23. package/dist/facets/opencode-staging.d.ts.map +1 -1
  24. package/dist/facets/opencode-staging.js +3 -4
  25. package/dist/facets/real-vite-hmr.js +1 -1
  26. package/dist/facets/vite-dev-server.d.ts +4 -4
  27. package/dist/facets/vite-dev-server.js +5 -5
  28. package/dist/git/commands.d.ts +22 -0
  29. package/dist/git/commands.d.ts.map +1 -1
  30. package/dist/git/commands.js +431 -424
  31. package/dist/git/network-facet.d.ts.map +1 -1
  32. package/dist/git/network-facet.js +6 -5
  33. package/dist/git-bundle.generated.d.ts +2 -2
  34. package/dist/git-bundle.generated.d.ts.map +1 -1
  35. package/dist/git-bundle.generated.js +3 -3
  36. package/dist/index.d.ts +2 -1
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +11 -2
  39. package/dist/loaders/child-process/spawn-facet.js +1 -1
  40. package/dist/loaders/child-process/spawn-pool.d.ts.map +1 -1
  41. package/dist/loaders/child-process/spawn-pool.js +2 -2
  42. package/dist/loaders/generated-workers.d.ts +2 -2
  43. package/dist/loaders/generated-workers.d.ts.map +1 -1
  44. package/dist/loaders/generated-workers.js +3 -3
  45. package/dist/loaders/npm-resolve-preamble.d.ts +3 -3
  46. package/dist/loaders/npm-resolve-preamble.js +3 -3
  47. package/dist/loaders/pre-bundle-preamble.d.ts +5 -5
  48. package/dist/loaders/pre-bundle-preamble.js +6 -6
  49. package/dist/node-shims-artifact.generated.js +3 -3
  50. package/dist/npm/install-batch-facet.d.ts +1 -1
  51. package/dist/npm/install-batch-facet.js +1 -1
  52. package/dist/npm/installer.d.ts +6 -6
  53. package/dist/npm/installer.d.ts.map +1 -1
  54. package/dist/npm/installer.js +28 -27
  55. package/dist/npm/pre-bundle-facet.d.ts +2 -2
  56. package/dist/npm/pre-bundle-facet.js +5 -5
  57. package/dist/npm/r2-cache.d.ts +1 -1
  58. package/dist/npm/resolve-facet.d.ts +1 -1
  59. package/dist/npm/resolve-facet.js +1 -1
  60. package/dist/npm/resolve-one-facet.d.ts +5 -5
  61. package/dist/npm/resolve-one-facet.js +5 -5
  62. package/dist/router/index.d.ts.map +1 -1
  63. package/dist/router/index.js +61 -4
  64. package/dist/router/public-directory-do.d.ts +17 -0
  65. package/dist/router/public-directory-do.d.ts.map +1 -0
  66. package/dist/router/public-directory-do.js +16 -0
  67. package/dist/router/public-directory.d.ts +75 -0
  68. package/dist/router/public-directory.d.ts.map +1 -0
  69. package/dist/router/public-directory.js +115 -0
  70. package/dist/router/remote-api.d.ts +9 -1
  71. package/dist/router/remote-api.d.ts.map +1 -1
  72. package/dist/router/remote-api.js +20 -3
  73. package/dist/runtime/bun-repl.d.ts +1 -1
  74. package/dist/runtime/bun-repl.js +3 -3
  75. package/dist/runtime/esbuild-wasm-bytes.js +1 -1
  76. package/dist/runtime/facet-loader-host.d.ts +2 -2
  77. package/dist/runtime/facet-loader-host.js +3 -3
  78. package/dist/runtime/node-repl.js +2 -2
  79. package/dist/runtime/node-shims-artifact.js +1 -1
  80. package/dist/runtime/node-shims.d.ts +4 -1
  81. package/dist/runtime/node-shims.d.ts.map +1 -1
  82. package/dist/runtime/node-shims.js +326 -7
  83. package/dist/runtime/opencode-artifact.js +1 -1
  84. package/dist/runtime/opentui-wasm-bytes.js +1 -1
  85. package/dist/runtime/python-repl.js +3 -3
  86. package/dist/runtime/ruby-repl.js +2 -2
  87. package/dist/runtime/sqlite-wasm-bytes.js +1 -1
  88. package/dist/session/agent.d.ts +11 -0
  89. package/dist/session/agent.d.ts.map +1 -1
  90. package/dist/session/diag.js +1 -1
  91. package/dist/session/helpers.js +1 -1
  92. package/dist/session/hibernation.d.ts +8 -8
  93. package/dist/session/hibernation.d.ts.map +1 -1
  94. package/dist/session/hibernation.js +15 -15
  95. package/dist/session/init-phases.d.ts +19 -26
  96. package/dist/session/init-phases.d.ts.map +1 -1
  97. package/dist/session/init-phases.js +20 -43
  98. package/dist/session/init.d.ts.map +1 -1
  99. package/dist/session/init.js +24 -19
  100. package/dist/session/keys.d.ts +8 -0
  101. package/dist/session/keys.d.ts.map +1 -1
  102. package/dist/session/keys.js +12 -3
  103. package/dist/session/nimbus-session.d.ts +33 -19
  104. package/dist/session/nimbus-session.d.ts.map +1 -1
  105. package/dist/session/nimbus-session.js +78 -37
  106. package/dist/session/port-capability.d.ts +95 -8
  107. package/dist/session/port-capability.d.ts.map +1 -1
  108. package/dist/session/port-capability.js +165 -8
  109. package/dist/session/programmatic.d.ts +50 -4
  110. package/dist/session/programmatic.d.ts.map +1 -1
  111. package/dist/session/programmatic.js +112 -15
  112. package/dist/session/routes.d.ts +17 -0
  113. package/dist/session/routes.d.ts.map +1 -1
  114. package/dist/session/routes.js +111 -43
  115. package/dist/session/rpc.d.ts +7 -7
  116. package/dist/session/rpc.d.ts.map +1 -1
  117. package/dist/session/rpc.js +34 -23
  118. package/dist/session/shell-socket.d.ts +76 -0
  119. package/dist/session/shell-socket.d.ts.map +1 -0
  120. package/dist/session/shell-socket.js +172 -0
  121. package/dist/session/start-real-vite.js +2 -2
  122. package/dist/session/supervisor-op.d.ts +5 -0
  123. package/dist/session/supervisor-op.d.ts.map +1 -0
  124. package/dist/session/supervisor-op.js +74 -0
  125. package/dist/session/supervisor-rpc.d.ts +6 -10
  126. package/dist/session/supervisor-rpc.d.ts.map +1 -1
  127. package/dist/session/supervisor-rpc.js +98 -79
  128. package/dist/session/ws.d.ts +1 -3
  129. package/dist/session/ws.d.ts.map +1 -1
  130. package/dist/session/ws.js +23 -7
  131. package/package.json +4 -3
  132. package/public/_assets/runtime/{node-shims-fea906843f6d594e.js → node-shims-9e1380d9ee430fcc.js} +320 -5
  133. package/public/s/index.html +101 -5
  134. package/scripts/bundle-esbuild-wasm.mjs +2 -2
  135. package/scripts/bundle-facet-workers.mjs +5 -7
  136. package/scripts/bundle-sqlite-wasm.mjs +1 -1
@@ -246,474 +246,481 @@ function getAuthor(ctx) {
246
246
  }
247
247
  // ── Git subcommand implementations ──────────────────────────────────────
248
248
  export function registerGitCommands(registry, vfs, doCtx, doEnv) {
249
- registry.register('git', async (ctx) => {
250
- const credentialedVfs = vfs.as(requireVfsCred(ctx.cred, 'git'));
251
- const fs = createGitFs(credentialedVfs);
252
- const args = ctx.args;
253
- const sub = args[0];
254
- const subArgs = args.slice(1);
255
- const dir = getDir(ctx);
256
- if (sub === '--version' || sub === '-v') {
257
- ctx.stdout.write('git version 2.44.0 (isomorphic-git/cf-git)\n');
258
- return 0;
259
- }
260
- if (!sub || sub === '--help' || sub === '-h') {
261
- ctx.stdout.write('usage: git <command> [<args>]\n\n');
262
- ctx.stdout.write('Commands:\n');
263
- ctx.stdout.write(' init, clone, status, add, commit, log, branch,\n');
264
- ctx.stdout.write(' checkout, diff, remote, fetch, pull, push, merge,\n');
265
- ctx.stdout.write(' reset, tag, config, --version\n');
266
- return 0;
267
- }
268
- // Lazy-load isomorphic-git only when actually needed.
269
- // Note: http transport isn't loaded here — network ops (clone/fetch/pull)
270
- // run inside the git-network-facet which imports its own http transport.
271
- let git;
272
- try {
273
- git = await getGit();
274
- }
275
- catch (e) {
276
- ctx.stderr.write(`git: failed to load git module: ${e?.message}\n`);
277
- return 1;
278
- }
279
- try {
280
- switch (sub) {
281
- case 'init': {
282
- // git init [path] — if path given, use it; otherwise use cwd
283
- let initDir = dir;
284
- const initPath = subArgs.find((a) => !a.startsWith('-'));
285
- if (initPath) {
286
- initDir = initPath.startsWith('/') ? initPath : dir + '/' + initPath;
287
- // Ensure the target directory exists in VFS
288
- const stripped = initDir.replace(/^\/+/, '');
289
- if (!credentialedVfs.exists(stripped))
290
- credentialedVfs.mkdir(stripped, { recursive: true });
291
- }
292
- await git.init({ fs, dir: initDir });
293
- ctx.stdout.write(`Initialized empty Git repository in ${initDir}/.git/\n`);
294
- return 0;
249
+ registry.register('git', (ctx) => runGitCommand(ctx, vfs, doCtx, doEnv));
250
+ }
251
+ /**
252
+ * The `git` command handler. Split out from registration so it can be
253
+ * lazy-loaded (`await import('./commands.js')`) on first `git` use, keeping
254
+ * this module and its ~106 KB network-facet dependency out of the cold
255
+ * script-eval graph.
256
+ */
257
+ export async function runGitCommand(ctx, vfs, doCtx, doEnv) {
258
+ const credentialedVfs = vfs.as(requireVfsCred(ctx.cred, 'git'));
259
+ const fs = createGitFs(credentialedVfs);
260
+ const args = ctx.args;
261
+ const sub = args[0];
262
+ const subArgs = args.slice(1);
263
+ const dir = getDir(ctx);
264
+ if (sub === '--version' || sub === '-v') {
265
+ ctx.stdout.write('git version 2.44.0 (isomorphic-git/cf-git)\n');
266
+ return 0;
267
+ }
268
+ if (!sub || sub === '--help' || sub === '-h') {
269
+ ctx.stdout.write('usage: git <command> [<args>]\n\n');
270
+ ctx.stdout.write('Commands:\n');
271
+ ctx.stdout.write(' init, clone, status, add, commit, log, branch,\n');
272
+ ctx.stdout.write(' checkout, diff, remote, fetch, pull, push, merge,\n');
273
+ ctx.stdout.write(' reset, tag, config, --version\n');
274
+ return 0;
275
+ }
276
+ // Lazy-load isomorphic-git only when actually needed.
277
+ // Note: http transport isn't loaded here — network ops (clone/fetch/pull)
278
+ // run inside the git-network-facet which imports its own http transport.
279
+ let git;
280
+ try {
281
+ git = await getGit();
282
+ }
283
+ catch (e) {
284
+ ctx.stderr.write(`git: failed to load git module: ${e?.message}\n`);
285
+ return 1;
286
+ }
287
+ try {
288
+ switch (sub) {
289
+ case 'init': {
290
+ // git init [path] — if path given, use it; otherwise use cwd
291
+ let initDir = dir;
292
+ const initPath = subArgs.find((a) => !a.startsWith('-'));
293
+ if (initPath) {
294
+ initDir = initPath.startsWith('/') ? initPath : dir + '/' + initPath;
295
+ // Ensure the target directory exists in VFS
296
+ const stripped = initDir.replace(/^\/+/, '');
297
+ if (!credentialedVfs.exists(stripped))
298
+ credentialedVfs.mkdir(stripped, { recursive: true });
295
299
  }
296
- case 'clone': {
297
- const { url, dest: destArg, depth, isBg, branch } = parseCloneArgs(subArgs);
298
- if (!url) {
299
- ctx.stderr.write(CLONE_USAGE + '\n');
300
- return 1;
301
- }
302
- // hardening-r5: respect absolute paths. Pre-fix `git clone <url> /tmp/x`
303
- // resolved to `<cwd>//tmp/x` because the `subArgs[1]` branch
304
- // unconditionally prepended getDir(ctx). The clone "succeeded" into
305
- // <cwd>//tmp/x (note double slash) and the user's later `cd /tmp/x`
306
- // hit ENOENT. Real-world git treats absolute targets as absolute.
307
- let dest;
308
- if (destArg) {
309
- dest = destArg.startsWith('/')
310
- ? destArg
311
- : getDir(ctx) + '/' + destArg;
312
- }
313
- else {
314
- dest = dir + '/' + url.split('/').pop()?.replace('.git', '');
315
- }
316
- if (!doCtx || !doEnv) {
317
- ctx.stderr.write('[git] clone requires DO ctx + env (internal configuration error)\n');
318
- return 1;
319
- }
320
- ctx.stdout.write(`Cloning into '${dest}'...${depth ? ' (shallow, depth=' + depth + ')' : ''}\n`);
321
- // A clone's closed-world filesystem view is correct only while no
322
- // other session surface can mutate its destination subtree. Acquire
323
- // the lease before the facet performs its lstat/readdir emptiness
324
- // proof; the clone's W7 stream carries the opaque owner capability
325
- // through the trusted SupervisorRPC binding.
326
- const mutationLease = vfs.acquireExclusiveMutation(dest, {
327
- includeMissingAncestors: true,
328
- });
329
- // Delegate to git-network-facet: heavy packfile processing runs in
330
- // a dynamic worker with its own CPU budget, not the supervisor DO.
331
- const doClone = async () => {
332
- try {
333
- const result = await execGitNetwork(doCtx, doEnv, {
334
- op: 'clone',
335
- pid: ctx.pid,
336
- dir: dest,
337
- url,
338
- ref: branch,
339
- depth,
340
- exclusiveDestination: true,
341
- exclusiveMutationRoot: mutationLease.root,
342
- mutationOwner: mutationLease.owner,
343
- // Verification/tuning knob: force a small per-chunk entry bound
344
- // so ordinary repos exercise the multi-invocation chunked
345
- // checkout path. Unset in production → the 10k default applies.
346
- checkoutChunkMaxEntries: ctx.env.NIMBUS_GIT_CHECKOUT_CHUNK_ENTRIES
347
- ? Number(ctx.env.NIMBUS_GIT_CHECKOUT_CHUNK_ENTRIES) || undefined
348
- : undefined,
349
- auth: {
350
- username: ctx.env.GIT_USERNAME || '',
351
- password: ctx.env.GIT_PASSWORD || ctx.env.GIT_TOKEN || '',
352
- },
353
- });
354
- if (result.success) {
355
- ctx.stdout.write(`\n[git] clone complete (${result.filesWritten} files, ` +
356
- `${(result.bytesWritten / 1024).toFixed(1)}KB in ${(result.elapsed / 1000).toFixed(1)}s)\n`);
357
- }
358
- else {
359
- ctx.stderr.write(`\n[git] clone failed: ${result.error}\n`);
360
- }
361
- return result.success;
362
- }
363
- finally {
364
- vfs.releaseExclusiveMutation(mutationLease.owner);
365
- }
366
- };
367
- if (isBg) {
368
- const task = doClone();
369
- doCtx.waitUntil(task);
370
- ctx.stdout.write('[git] clone running in background...\n');
371
- return 0;
372
- }
373
- else {
374
- return (await doClone()) ? 0 : 1;
375
- }
300
+ await git.init({ fs, dir: initDir });
301
+ ctx.stdout.write(`Initialized empty Git repository in ${initDir}/.git/\n`);
302
+ return 0;
303
+ }
304
+ case 'clone': {
305
+ const { url, dest: destArg, depth, isBg, branch } = parseCloneArgs(subArgs);
306
+ if (!url) {
307
+ ctx.stderr.write(CLONE_USAGE + '\n');
308
+ return 1;
376
309
  }
377
- case 'status': {
378
- const matrix = await git.statusMatrix({ fs, dir });
379
- let clean = true;
380
- for (const [filepath, head, workdir, stage] of matrix) {
381
- if (head === workdir && workdir === stage)
382
- continue;
383
- clean = false;
384
- if (head === 0 && workdir === 2 && stage === 0)
385
- ctx.stdout.write(`\x1b[31m?? ${filepath}\x1b[0m\n`);
386
- else if (head === 0 && stage === 2)
387
- ctx.stdout.write(`\x1b[32mA ${filepath}\x1b[0m\n`);
388
- else if (head === 1 && workdir === 2 && stage === 2)
389
- ctx.stdout.write(`\x1b[32mM ${filepath}\x1b[0m\n`);
390
- else if (head === 1 && workdir === 2 && stage === 1)
391
- ctx.stdout.write(`\x1b[31m M ${filepath}\x1b[0m\n`);
392
- else if (head === 1 && workdir === 0)
393
- ctx.stdout.write(`\x1b[31m D ${filepath}\x1b[0m\n`);
394
- else if (head === 1 && stage === 0)
395
- ctx.stdout.write(`\x1b[32mD ${filepath}\x1b[0m\n`);
396
- else
397
- ctx.stdout.write(` ${filepath} [${head},${workdir},${stage}]\n`);
398
- }
399
- if (clean)
400
- ctx.stdout.write('nothing to commit, working tree clean\n');
401
- return 0;
310
+ // hardening-r5: respect absolute paths. Pre-fix `git clone <url> /tmp/x`
311
+ // resolved to `<cwd>//tmp/x` because the `subArgs[1]` branch
312
+ // unconditionally prepended getDir(ctx). The clone "succeeded" into
313
+ // <cwd>//tmp/x (note double slash) and the user's later `cd /tmp/x`
314
+ // hit ENOENT. Real-world git treats absolute targets as absolute.
315
+ let dest;
316
+ if (destArg) {
317
+ dest = destArg.startsWith('/')
318
+ ? destArg
319
+ : getDir(ctx) + '/' + destArg;
402
320
  }
403
- case 'add': {
404
- const paths = subArgs.filter(a => !a.startsWith('-'));
405
- if (paths.length === 0 || paths.includes('.')) {
406
- // Add all
407
- const matrix = await git.statusMatrix({ fs, dir });
408
- for (const [filepath, head, workdir, stage] of matrix) {
409
- if (head !== workdir || workdir !== stage) {
410
- if (workdir === 0)
411
- await git.remove({ fs, dir, filepath });
412
- else
413
- await git.add({ fs, dir, filepath });
414
- }
415
- }
416
- }
417
- else {
418
- for (const filepath of paths) {
419
- await git.add({ fs, dir, filepath });
420
- }
421
- }
422
- return 0;
321
+ else {
322
+ dest = dir + '/' + url.split('/').pop()?.replace('.git', '');
423
323
  }
424
- case 'commit': {
425
- const msgIdx = subArgs.indexOf('-m');
426
- const message = msgIdx >= 0 ? subArgs[msgIdx + 1] : 'commit';
427
- if (!message) {
428
- ctx.stderr.write('error: empty commit message\n');
429
- return 1;
430
- }
431
- const sha = await git.commit({
432
- fs, dir, message,
433
- author: getAuthor(ctx),
434
- });
435
- ctx.stdout.write(`[${sha.slice(0, 7)}] ${message}\n`);
436
- return 0;
324
+ if (!doCtx || !doEnv) {
325
+ ctx.stderr.write('[git] clone requires DO ctx + env (internal configuration error)\n');
326
+ return 1;
437
327
  }
438
- case 'log': {
439
- const maxCount = parseInt(getFlag(subArgs, '-n') || getFlag(subArgs, '--max-count') || '10');
440
- const oneline = subArgs.includes('--oneline');
441
- const commits = await git.log({ fs, dir, depth: maxCount });
442
- for (const c of commits) {
443
- if (oneline) {
444
- ctx.stdout.write(`\x1b[33m${c.oid.slice(0, 7)}\x1b[0m ${c.commit.message.split('\n')[0]}\n`);
328
+ ctx.stdout.write(`Cloning into '${dest}'...${depth ? ' (shallow, depth=' + depth + ')' : ''}\n`);
329
+ // A clone's closed-world filesystem view is correct only while no
330
+ // other session surface can mutate its destination subtree. Acquire
331
+ // the lease before the facet performs its lstat/readdir emptiness
332
+ // proof; the clone's W7 stream carries the opaque owner capability
333
+ // through the trusted SupervisorRPC binding.
334
+ const mutationLease = vfs.acquireExclusiveMutation(dest, {
335
+ includeMissingAncestors: true,
336
+ });
337
+ // Delegate to git-network-facet: heavy packfile processing runs in
338
+ // a dynamic worker with its own CPU budget, not the supervisor DO.
339
+ const doClone = async () => {
340
+ try {
341
+ const result = await execGitNetwork(doCtx, doEnv, {
342
+ op: 'clone',
343
+ pid: ctx.pid,
344
+ dir: dest,
345
+ url,
346
+ ref: branch,
347
+ depth,
348
+ exclusiveDestination: true,
349
+ exclusiveMutationRoot: mutationLease.root,
350
+ mutationOwner: mutationLease.owner,
351
+ // Verification/tuning knob: force a small per-chunk entry bound
352
+ // so ordinary repos exercise the multi-invocation chunked
353
+ // checkout path. Unset in production → the 10k default applies.
354
+ checkoutChunkMaxEntries: ctx.env.NIMBUS_GIT_CHECKOUT_CHUNK_ENTRIES
355
+ ? Number(ctx.env.NIMBUS_GIT_CHECKOUT_CHUNK_ENTRIES) || undefined
356
+ : undefined,
357
+ auth: {
358
+ username: ctx.env.GIT_USERNAME || '',
359
+ password: ctx.env.GIT_PASSWORD || ctx.env.GIT_TOKEN || '',
360
+ },
361
+ });
362
+ if (result.success) {
363
+ ctx.stdout.write(`\n[git] clone complete (${result.filesWritten} files, ` +
364
+ `${(result.bytesWritten / 1024).toFixed(1)}KB in ${(result.elapsed / 1000).toFixed(1)}s)\n`);
445
365
  }
446
366
  else {
447
- ctx.stdout.write(`\x1b[33mcommit ${c.oid}\x1b[0m\n`);
448
- ctx.stdout.write(`Author: ${c.commit.author.name} <${c.commit.author.email}>\n`);
449
- ctx.stdout.write(`Date: ${new Date(c.commit.author.timestamp * 1000).toDateString()}\n\n`);
450
- ctx.stdout.write(` ${c.commit.message}\n\n`);
367
+ ctx.stderr.write(`\n[git] clone failed: ${result.error}\n`);
451
368
  }
369
+ return result.success;
452
370
  }
453
- return 0;
454
- }
455
- case 'branch': {
456
- if (subArgs.length === 0 || subArgs[0] === '-a' || subArgs[0] === '--list') {
457
- const branches = await git.listBranches({ fs, dir });
458
- const current = await git.currentBranch({ fs, dir });
459
- for (const b of branches) {
460
- ctx.stdout.write(b === current ? `\x1b[32m* ${b}\x1b[0m\n` : ` ${b}\n`);
461
- }
462
- if (subArgs.includes('-a')) {
463
- try {
464
- const remotes = await git.listBranches({ fs, dir, remote: 'origin' });
465
- for (const b of remotes)
466
- ctx.stdout.write(` \x1b[31mremotes/origin/${b}\x1b[0m\n`);
467
- }
468
- catch { }
469
- }
470
- }
471
- else if (subArgs.includes('-d') || subArgs.includes('-D')) {
472
- const name = subArgs.find(a => !a.startsWith('-'));
473
- if (name) {
474
- await git.deleteBranch({ fs, dir, ref: name });
475
- ctx.stdout.write(`Deleted branch ${name}\n`);
476
- }
477
- }
478
- else {
479
- const name = subArgs[0];
480
- await git.branch({ fs, dir, ref: name });
481
- ctx.stdout.write(`Created branch ${name}\n`);
371
+ finally {
372
+ vfs.releaseExclusiveMutation(mutationLease.owner);
482
373
  }
374
+ };
375
+ if (isBg) {
376
+ const task = doClone();
377
+ doCtx.waitUntil(task);
378
+ ctx.stdout.write('[git] clone running in background...\n');
483
379
  return 0;
484
380
  }
485
- case 'checkout': {
486
- const ref = subArgs.find(a => !a.startsWith('-'));
487
- if (!ref) {
488
- ctx.stderr.write('error: specify a branch\n');
489
- return 1;
490
- }
491
- if (subArgs.includes('-b')) {
492
- await git.branch({ fs, dir, ref });
493
- await git.checkout({ fs, dir, ref });
494
- ctx.stdout.write(`Switched to a new branch '${ref}'\n`);
495
- }
496
- else {
497
- await git.checkout({ fs, dir, ref });
498
- ctx.stdout.write(`Switched to branch '${ref}'\n`);
499
- }
500
- return 0;
381
+ else {
382
+ return (await doClone()) ? 0 : 1;
501
383
  }
502
- case 'diff': {
503
- // Simple diff: show unstaged changes
384
+ }
385
+ case 'status': {
386
+ const matrix = await git.statusMatrix({ fs, dir });
387
+ let clean = true;
388
+ for (const [filepath, head, workdir, stage] of matrix) {
389
+ if (head === workdir && workdir === stage)
390
+ continue;
391
+ clean = false;
392
+ if (head === 0 && workdir === 2 && stage === 0)
393
+ ctx.stdout.write(`\x1b[31m?? ${filepath}\x1b[0m\n`);
394
+ else if (head === 0 && stage === 2)
395
+ ctx.stdout.write(`\x1b[32mA ${filepath}\x1b[0m\n`);
396
+ else if (head === 1 && workdir === 2 && stage === 2)
397
+ ctx.stdout.write(`\x1b[32mM ${filepath}\x1b[0m\n`);
398
+ else if (head === 1 && workdir === 2 && stage === 1)
399
+ ctx.stdout.write(`\x1b[31m M ${filepath}\x1b[0m\n`);
400
+ else if (head === 1 && workdir === 0)
401
+ ctx.stdout.write(`\x1b[31m D ${filepath}\x1b[0m\n`);
402
+ else if (head === 1 && stage === 0)
403
+ ctx.stdout.write(`\x1b[32mD ${filepath}\x1b[0m\n`);
404
+ else
405
+ ctx.stdout.write(` ${filepath} [${head},${workdir},${stage}]\n`);
406
+ }
407
+ if (clean)
408
+ ctx.stdout.write('nothing to commit, working tree clean\n');
409
+ return 0;
410
+ }
411
+ case 'add': {
412
+ const paths = subArgs.filter(a => !a.startsWith('-'));
413
+ if (paths.length === 0 || paths.includes('.')) {
414
+ // Add all
504
415
  const matrix = await git.statusMatrix({ fs, dir });
505
416
  for (const [filepath, head, workdir, stage] of matrix) {
506
- if (workdir !== head || workdir !== stage) {
507
- ctx.stdout.write(`\x1b[1mdiff --git a/${filepath} b/${filepath}\x1b[0m\n`);
508
- try {
509
- const raw = await fs.promises.readFile(dir + '/' + filepath);
510
- const content = typeof raw === 'string' ? raw : dec.decode(raw);
511
- const lines = content.split('\n');
512
- for (let i = 0; i < Math.min(lines.length, 50); i++) {
513
- if (head === 0)
514
- ctx.stdout.write(`\x1b[32m+${lines[i]}\x1b[0m\n`);
515
- else
516
- ctx.stdout.write(` ${lines[i]}\n`);
517
- }
518
- if (lines.length > 50)
519
- ctx.stdout.write(`... (${lines.length - 50} more lines)\n`);
520
- }
521
- catch { }
522
- ctx.stdout.write('\n');
417
+ if (head !== workdir || workdir !== stage) {
418
+ if (workdir === 0)
419
+ await git.remove({ fs, dir, filepath });
420
+ else
421
+ await git.add({ fs, dir, filepath });
523
422
  }
524
423
  }
525
- return 0;
526
424
  }
527
- case 'remote': {
528
- if (subArgs[0] === 'add' && subArgs[1] && subArgs[2]) {
529
- await git.addRemote({ fs, dir, remote: subArgs[1], url: subArgs[2] });
530
- ctx.stdout.write(`Remote '${subArgs[1]}' added\n`);
531
- }
532
- else if (subArgs[0] === 'remove' || subArgs[0] === 'rm') {
533
- await git.deleteRemote({ fs, dir, remote: subArgs[1] });
534
- ctx.stdout.write(`Remote '${subArgs[1]}' removed\n`);
535
- }
536
- else {
537
- const remotes = await git.listRemotes({ fs, dir });
538
- for (const r of remotes) {
539
- ctx.stdout.write(subArgs.includes('-v') ? `${r.remote}\t${r.url} (fetch)\n` : `${r.remote}\n`);
540
- }
425
+ else {
426
+ for (const filepath of paths) {
427
+ await git.add({ fs, dir, filepath });
541
428
  }
542
- return 0;
543
429
  }
544
- case 'fetch': {
545
- const remote = subArgs[0] || 'origin';
546
- if (!doCtx || !doEnv) {
547
- ctx.stderr.write('[git] fetch requires DO ctx + env (internal configuration error)\n');
548
- return 1;
549
- }
550
- ctx.stdout.write(`Fetching from ${remote}...\n`);
551
- const result = await execGitNetwork(doCtx, doEnv, {
552
- op: 'fetch',
553
- pid: ctx.pid,
554
- dir,
555
- remote,
556
- auth: {
557
- username: ctx.env.GIT_USERNAME || '',
558
- password: ctx.env.GIT_PASSWORD || ctx.env.GIT_TOKEN || '',
559
- },
560
- });
561
- if (result.success) {
562
- ctx.stdout.write(`\n[git] fetch complete (${result.filesWritten} files in ${(result.elapsed / 1000).toFixed(1)}s)\n`);
563
- return 0;
564
- }
565
- else {
566
- ctx.stderr.write(`\n[git] fetch failed: ${result.error}\n`);
567
- return 1;
568
- }
430
+ return 0;
431
+ }
432
+ case 'commit': {
433
+ const msgIdx = subArgs.indexOf('-m');
434
+ const message = msgIdx >= 0 ? subArgs[msgIdx + 1] : 'commit';
435
+ if (!message) {
436
+ ctx.stderr.write('error: empty commit message\n');
437
+ return 1;
569
438
  }
570
- case 'pull': {
571
- const remote = subArgs[0] || 'origin';
572
- const branch = subArgs[1] || await git.currentBranch({ fs, dir }) || 'main';
573
- if (!doCtx || !doEnv) {
574
- ctx.stderr.write('[git] pull requires DO ctx + env (internal configuration error)\n');
575
- return 1;
576
- }
577
- ctx.stdout.write(`Pulling from ${remote}/${branch}...\n`);
578
- const result = await execGitNetwork(doCtx, doEnv, {
579
- op: 'pull',
580
- pid: ctx.pid,
581
- dir,
582
- remote,
583
- ref: branch,
584
- author: getAuthor(ctx),
585
- auth: {
586
- username: ctx.env.GIT_USERNAME || '',
587
- password: ctx.env.GIT_PASSWORD || ctx.env.GIT_TOKEN || '',
588
- },
589
- });
590
- if (result.success) {
591
- ctx.stdout.write(`\n[git] pull complete (${result.filesWritten} files in ${(result.elapsed / 1000).toFixed(1)}s)\n`);
592
- return 0;
439
+ const sha = await git.commit({
440
+ fs, dir, message,
441
+ author: getAuthor(ctx),
442
+ });
443
+ ctx.stdout.write(`[${sha.slice(0, 7)}] ${message}\n`);
444
+ return 0;
445
+ }
446
+ case 'log': {
447
+ const maxCount = parseInt(getFlag(subArgs, '-n') || getFlag(subArgs, '--max-count') || '10');
448
+ const oneline = subArgs.includes('--oneline');
449
+ const commits = await git.log({ fs, dir, depth: maxCount });
450
+ for (const c of commits) {
451
+ if (oneline) {
452
+ ctx.stdout.write(`\x1b[33m${c.oid.slice(0, 7)}\x1b[0m ${c.commit.message.split('\n')[0]}\n`);
593
453
  }
594
454
  else {
595
- ctx.stderr.write(`\n[git] pull failed: ${result.error}\n`);
596
- return 1;
455
+ ctx.stdout.write(`\x1b[33mcommit ${c.oid}\x1b[0m\n`);
456
+ ctx.stdout.write(`Author: ${c.commit.author.name} <${c.commit.author.email}>\n`);
457
+ ctx.stdout.write(`Date: ${new Date(c.commit.author.timestamp * 1000).toDateString()}\n\n`);
458
+ ctx.stdout.write(` ${c.commit.message}\n\n`);
597
459
  }
598
460
  }
599
- case 'push': {
600
- const remote = subArgs[0] || 'origin';
601
- const branch = subArgs[1] || await git.currentBranch({ fs, dir }) || 'main';
602
- if (!doCtx || !doEnv) {
603
- ctx.stderr.write('[git] push requires DO ctx + env (internal configuration error)\n');
604
- return 1;
605
- }
606
- ctx.stdout.write(`Pushing to ${remote}/${branch}...\n`);
607
- const result = await execGitNetwork(doCtx, doEnv, {
608
- op: 'push',
609
- pid: ctx.pid,
610
- dir,
611
- remote,
612
- ref: branch,
613
- auth: {
614
- username: ctx.env.GIT_USERNAME || '',
615
- password: ctx.env.GIT_PASSWORD || ctx.env.GIT_TOKEN || '',
616
- },
617
- });
618
- if (result.success) {
619
- ctx.stdout.write(`\n[git] push complete (${(result.elapsed / 1000).toFixed(1)}s)\n`);
620
- return 0;
621
- }
622
- else {
623
- ctx.stderr.write(`\n[git] push failed: ${result.error}\n`);
624
- return 1;
461
+ return 0;
462
+ }
463
+ case 'branch': {
464
+ if (subArgs.length === 0 || subArgs[0] === '-a' || subArgs[0] === '--list') {
465
+ const branches = await git.listBranches({ fs, dir });
466
+ const current = await git.currentBranch({ fs, dir });
467
+ for (const b of branches) {
468
+ ctx.stdout.write(b === current ? `\x1b[32m* ${b}\x1b[0m\n` : ` ${b}\n`);
469
+ }
470
+ if (subArgs.includes('-a')) {
471
+ try {
472
+ const remotes = await git.listBranches({ fs, dir, remote: 'origin' });
473
+ for (const b of remotes)
474
+ ctx.stdout.write(` \x1b[31mremotes/origin/${b}\x1b[0m\n`);
475
+ }
476
+ catch { }
625
477
  }
626
478
  }
627
- case 'merge': {
628
- const theirs = subArgs[0];
629
- if (!theirs) {
630
- ctx.stderr.write('usage: git merge <branch>\n');
631
- return 1;
479
+ else if (subArgs.includes('-d') || subArgs.includes('-D')) {
480
+ const name = subArgs.find(a => !a.startsWith('-'));
481
+ if (name) {
482
+ await git.deleteBranch({ fs, dir, ref: name });
483
+ ctx.stdout.write(`Deleted branch ${name}\n`);
632
484
  }
633
- await git.merge({
634
- fs, dir, theirs,
635
- author: getAuthor(ctx),
636
- });
637
- ctx.stdout.write(`Merged ${theirs}\n`);
638
- return 0;
639
485
  }
640
- case 'reset': {
641
- const hard = subArgs.includes('--hard');
642
- const soft = subArgs.includes('--soft');
643
- const ref = subArgs.find(a => !a.startsWith('-')) || 'HEAD';
644
- const oid = await git.resolveRef({ fs, dir, ref });
645
- // Move the current branch to the target OID
646
- const branch = await git.currentBranch({ fs, dir });
647
- if (branch) {
648
- await git.writeRef({ fs, dir, ref: `refs/heads/${branch}`, value: oid, force: true });
649
- }
650
- if (!soft) {
651
- // Reset index (--mixed behavior, also applies to --hard)
652
- const matrix = await git.statusMatrix({ fs, dir });
653
- for (const [filepath] of matrix) {
654
- try {
655
- await git.resetIndex({ fs, dir, filepath });
486
+ else {
487
+ const name = subArgs[0];
488
+ await git.branch({ fs, dir, ref: name });
489
+ ctx.stdout.write(`Created branch ${name}\n`);
490
+ }
491
+ return 0;
492
+ }
493
+ case 'checkout': {
494
+ const ref = subArgs.find(a => !a.startsWith('-'));
495
+ if (!ref) {
496
+ ctx.stderr.write('error: specify a branch\n');
497
+ return 1;
498
+ }
499
+ if (subArgs.includes('-b')) {
500
+ await git.branch({ fs, dir, ref });
501
+ await git.checkout({ fs, dir, ref });
502
+ ctx.stdout.write(`Switched to a new branch '${ref}'\n`);
503
+ }
504
+ else {
505
+ await git.checkout({ fs, dir, ref });
506
+ ctx.stdout.write(`Switched to branch '${ref}'\n`);
507
+ }
508
+ return 0;
509
+ }
510
+ case 'diff': {
511
+ // Simple diff: show unstaged changes
512
+ const matrix = await git.statusMatrix({ fs, dir });
513
+ for (const [filepath, head, workdir, stage] of matrix) {
514
+ if (workdir !== head || workdir !== stage) {
515
+ ctx.stdout.write(`\x1b[1mdiff --git a/${filepath} b/${filepath}\x1b[0m\n`);
516
+ try {
517
+ const raw = await fs.promises.readFile(dir + '/' + filepath);
518
+ const content = typeof raw === 'string' ? raw : dec.decode(raw);
519
+ const lines = content.split('\n');
520
+ for (let i = 0; i < Math.min(lines.length, 50); i++) {
521
+ if (head === 0)
522
+ ctx.stdout.write(`\x1b[32m+${lines[i]}\x1b[0m\n`);
523
+ else
524
+ ctx.stdout.write(` ${lines[i]}\n`);
656
525
  }
657
- catch { }
526
+ if (lines.length > 50)
527
+ ctx.stdout.write(`... (${lines.length - 50} more lines)\n`);
658
528
  }
529
+ catch { }
530
+ ctx.stdout.write('\n');
659
531
  }
660
- if (hard) {
661
- // Reset working tree to match the target
662
- await git.checkout({ fs, dir, ref: oid, force: true });
532
+ }
533
+ return 0;
534
+ }
535
+ case 'remote': {
536
+ if (subArgs[0] === 'add' && subArgs[1] && subArgs[2]) {
537
+ await git.addRemote({ fs, dir, remote: subArgs[1], url: subArgs[2] });
538
+ ctx.stdout.write(`Remote '${subArgs[1]}' added\n`);
539
+ }
540
+ else if (subArgs[0] === 'remove' || subArgs[0] === 'rm') {
541
+ await git.deleteRemote({ fs, dir, remote: subArgs[1] });
542
+ ctx.stdout.write(`Remote '${subArgs[1]}' removed\n`);
543
+ }
544
+ else {
545
+ const remotes = await git.listRemotes({ fs, dir });
546
+ for (const r of remotes) {
547
+ ctx.stdout.write(subArgs.includes('-v') ? `${r.remote}\t${r.url} (fetch)\n` : `${r.remote}\n`);
663
548
  }
664
- ctx.stdout.write(`HEAD is now at ${oid.slice(0, 7)}\n`);
549
+ }
550
+ return 0;
551
+ }
552
+ case 'fetch': {
553
+ const remote = subArgs[0] || 'origin';
554
+ if (!doCtx || !doEnv) {
555
+ ctx.stderr.write('[git] fetch requires DO ctx + env (internal configuration error)\n');
556
+ return 1;
557
+ }
558
+ ctx.stdout.write(`Fetching from ${remote}...\n`);
559
+ const result = await execGitNetwork(doCtx, doEnv, {
560
+ op: 'fetch',
561
+ pid: ctx.pid,
562
+ dir,
563
+ remote,
564
+ auth: {
565
+ username: ctx.env.GIT_USERNAME || '',
566
+ password: ctx.env.GIT_PASSWORD || ctx.env.GIT_TOKEN || '',
567
+ },
568
+ });
569
+ if (result.success) {
570
+ ctx.stdout.write(`\n[git] fetch complete (${result.filesWritten} files in ${(result.elapsed / 1000).toFixed(1)}s)\n`);
665
571
  return 0;
666
572
  }
667
- case 'tag': {
668
- if (subArgs.length === 0) {
669
- const tags = await git.listTags({ fs, dir });
670
- for (const t of tags)
671
- ctx.stdout.write(t + '\n');
672
- }
673
- else if (subArgs.includes('-d')) {
674
- const name = subArgs.find(a => !a.startsWith('-'));
675
- if (name)
676
- await git.deleteTag({ fs, dir, ref: name });
677
- }
678
- else {
679
- const name = subArgs[0];
680
- await git.tag({ fs, dir, ref: name });
681
- ctx.stdout.write(`Created tag ${name}\n`);
682
- }
573
+ else {
574
+ ctx.stderr.write(`\n[git] fetch failed: ${result.error}\n`);
575
+ return 1;
576
+ }
577
+ }
578
+ case 'pull': {
579
+ const remote = subArgs[0] || 'origin';
580
+ const branch = subArgs[1] || await git.currentBranch({ fs, dir }) || 'main';
581
+ if (!doCtx || !doEnv) {
582
+ ctx.stderr.write('[git] pull requires DO ctx + env (internal configuration error)\n');
583
+ return 1;
584
+ }
585
+ ctx.stdout.write(`Pulling from ${remote}/${branch}...\n`);
586
+ const result = await execGitNetwork(doCtx, doEnv, {
587
+ op: 'pull',
588
+ pid: ctx.pid,
589
+ dir,
590
+ remote,
591
+ ref: branch,
592
+ author: getAuthor(ctx),
593
+ auth: {
594
+ username: ctx.env.GIT_USERNAME || '',
595
+ password: ctx.env.GIT_PASSWORD || ctx.env.GIT_TOKEN || '',
596
+ },
597
+ });
598
+ if (result.success) {
599
+ ctx.stdout.write(`\n[git] pull complete (${result.filesWritten} files in ${(result.elapsed / 1000).toFixed(1)}s)\n`);
683
600
  return 0;
684
601
  }
685
- case 'config': {
686
- const key = subArgs.find(a => !a.startsWith('-'));
687
- const value = subArgs[subArgs.indexOf(key || '') + 1];
688
- if (key && value) {
689
- const [section, ...rest] = key.split('.');
690
- await git.setConfig({ fs, dir, path: key, value });
691
- ctx.stdout.write(`${key}=${value}\n`);
692
- }
693
- else if (key) {
602
+ else {
603
+ ctx.stderr.write(`\n[git] pull failed: ${result.error}\n`);
604
+ return 1;
605
+ }
606
+ }
607
+ case 'push': {
608
+ const remote = subArgs[0] || 'origin';
609
+ const branch = subArgs[1] || await git.currentBranch({ fs, dir }) || 'main';
610
+ if (!doCtx || !doEnv) {
611
+ ctx.stderr.write('[git] push requires DO ctx + env (internal configuration error)\n');
612
+ return 1;
613
+ }
614
+ ctx.stdout.write(`Pushing to ${remote}/${branch}...\n`);
615
+ const result = await execGitNetwork(doCtx, doEnv, {
616
+ op: 'push',
617
+ pid: ctx.pid,
618
+ dir,
619
+ remote,
620
+ ref: branch,
621
+ auth: {
622
+ username: ctx.env.GIT_USERNAME || '',
623
+ password: ctx.env.GIT_PASSWORD || ctx.env.GIT_TOKEN || '',
624
+ },
625
+ });
626
+ if (result.success) {
627
+ ctx.stdout.write(`\n[git] push complete (${(result.elapsed / 1000).toFixed(1)}s)\n`);
628
+ return 0;
629
+ }
630
+ else {
631
+ ctx.stderr.write(`\n[git] push failed: ${result.error}\n`);
632
+ return 1;
633
+ }
634
+ }
635
+ case 'merge': {
636
+ const theirs = subArgs[0];
637
+ if (!theirs) {
638
+ ctx.stderr.write('usage: git merge <branch>\n');
639
+ return 1;
640
+ }
641
+ await git.merge({
642
+ fs, dir, theirs,
643
+ author: getAuthor(ctx),
644
+ });
645
+ ctx.stdout.write(`Merged ${theirs}\n`);
646
+ return 0;
647
+ }
648
+ case 'reset': {
649
+ const hard = subArgs.includes('--hard');
650
+ const soft = subArgs.includes('--soft');
651
+ const ref = subArgs.find(a => !a.startsWith('-')) || 'HEAD';
652
+ const oid = await git.resolveRef({ fs, dir, ref });
653
+ // Move the current branch to the target OID
654
+ const branch = await git.currentBranch({ fs, dir });
655
+ if (branch) {
656
+ await git.writeRef({ fs, dir, ref: `refs/heads/${branch}`, value: oid, force: true });
657
+ }
658
+ if (!soft) {
659
+ // Reset index (--mixed behavior, also applies to --hard)
660
+ const matrix = await git.statusMatrix({ fs, dir });
661
+ for (const [filepath] of matrix) {
694
662
  try {
695
- const val = await git.getConfig({ fs, dir, path: key });
696
- ctx.stdout.write(`${val}\n`);
697
- }
698
- catch {
699
- ctx.stderr.write(`config: key '${key}' not set\n`);
700
- return 1;
663
+ await git.resetIndex({ fs, dir, filepath });
701
664
  }
665
+ catch { }
702
666
  }
703
- else {
704
- ctx.stderr.write('usage: git config <key> [value]\n');
667
+ }
668
+ if (hard) {
669
+ // Reset working tree to match the target
670
+ await git.checkout({ fs, dir, ref: oid, force: true });
671
+ }
672
+ ctx.stdout.write(`HEAD is now at ${oid.slice(0, 7)}\n`);
673
+ return 0;
674
+ }
675
+ case 'tag': {
676
+ if (subArgs.length === 0) {
677
+ const tags = await git.listTags({ fs, dir });
678
+ for (const t of tags)
679
+ ctx.stdout.write(t + '\n');
680
+ }
681
+ else if (subArgs.includes('-d')) {
682
+ const name = subArgs.find(a => !a.startsWith('-'));
683
+ if (name)
684
+ await git.deleteTag({ fs, dir, ref: name });
685
+ }
686
+ else {
687
+ const name = subArgs[0];
688
+ await git.tag({ fs, dir, ref: name });
689
+ ctx.stdout.write(`Created tag ${name}\n`);
690
+ }
691
+ return 0;
692
+ }
693
+ case 'config': {
694
+ const key = subArgs.find(a => !a.startsWith('-'));
695
+ const value = subArgs[subArgs.indexOf(key || '') + 1];
696
+ if (key && value) {
697
+ const [section, ...rest] = key.split('.');
698
+ await git.setConfig({ fs, dir, path: key, value });
699
+ ctx.stdout.write(`${key}=${value}\n`);
700
+ }
701
+ else if (key) {
702
+ try {
703
+ const val = await git.getConfig({ fs, dir, path: key });
704
+ ctx.stdout.write(`${val}\n`);
705
+ }
706
+ catch {
707
+ ctx.stderr.write(`config: key '${key}' not set\n`);
705
708
  return 1;
706
709
  }
707
- return 0;
708
710
  }
709
- default:
710
- ctx.stderr.write(`git: '${sub}' is not a git command. See 'git --help'.\n`);
711
+ else {
712
+ ctx.stderr.write('usage: git config <key> [value]\n');
711
713
  return 1;
714
+ }
715
+ return 0;
712
716
  }
717
+ default:
718
+ ctx.stderr.write(`git: '${sub}' is not a git command. See 'git --help'.\n`);
719
+ return 1;
713
720
  }
714
- catch (e) {
715
- ctx.stderr.write(`fatal: ${e?.message || e}\n`);
716
- return 128;
717
- }
718
- });
721
+ }
722
+ catch (e) {
723
+ ctx.stderr.write(`fatal: ${e?.message || e}\n`);
724
+ return 128;
725
+ }
719
726
  }