@prover-coder-ai/docker-git 1.0.36 → 1.0.38

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.
@@ -345,6 +345,228 @@ const runCommandCapture = (spec, okExitCodes, onFailure) => Effect.scoped(
345
345
  return new TextDecoder("utf-8").decode(bytes);
346
346
  })
347
347
  );
348
+ const resolveDockerEnvValue = (key) => {
349
+ const value = process.env[key]?.trim();
350
+ return value && value.length > 0 ? value : null;
351
+ };
352
+ const trimDockerPathTrailingSlash = (value) => {
353
+ let end = value.length;
354
+ while (end > 0) {
355
+ const char = value[end - 1];
356
+ if (char !== "/" && char !== "\\") {
357
+ break;
358
+ }
359
+ end -= 1;
360
+ }
361
+ return value.slice(0, end);
362
+ };
363
+ const pathStartsWith = (candidate, prefix) => candidate === prefix || candidate.startsWith(`${prefix}/`) || candidate.startsWith(`${prefix}\\`);
364
+ const translatePathPrefix = (candidate, sourcePrefix, targetPrefix) => pathStartsWith(candidate, sourcePrefix) ? `${targetPrefix}${candidate.slice(sourcePrefix.length)}` : null;
365
+ const resolveContainerProjectsRoot = () => {
366
+ const explicit = resolveDockerEnvValue("DOCKER_GIT_PROJECTS_ROOT");
367
+ if (explicit !== null) {
368
+ return explicit;
369
+ }
370
+ const home = resolveDockerEnvValue("HOME") ?? resolveDockerEnvValue("USERPROFILE");
371
+ return home === null ? null : `${trimDockerPathTrailingSlash(home)}/.docker-git`;
372
+ };
373
+ const resolveProjectsRootHostOverride = () => resolveDockerEnvValue("DOCKER_GIT_PROJECTS_ROOT_HOST");
374
+ const resolveCurrentContainerId = (cwd) => {
375
+ const fromEnv = resolveDockerEnvValue("HOSTNAME");
376
+ if (fromEnv !== null) {
377
+ return Effect.succeed(fromEnv);
378
+ }
379
+ return runCommandCapture(
380
+ {
381
+ cwd,
382
+ command: "hostname",
383
+ args: []
384
+ },
385
+ [0],
386
+ () => new Error("hostname failed")
387
+ ).pipe(
388
+ Effect.map((value) => value.trim()),
389
+ Effect.orElseSucceed(() => ""),
390
+ Effect.map((value) => value.length > 0 ? value : null)
391
+ );
392
+ };
393
+ const parseDockerInspectMounts = (raw) => raw.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0).flatMap((line) => {
394
+ const separator = line.indexOf(" ");
395
+ if (separator <= 0 || separator >= line.length - 1) {
396
+ return [];
397
+ }
398
+ const source = line.slice(0, separator).trim();
399
+ const destination = line.slice(separator + 1).trim();
400
+ if (source.length === 0 || destination.length === 0) {
401
+ return [];
402
+ }
403
+ return [{ source, destination }];
404
+ });
405
+ const remapDockerBindHostPathFromMounts = (hostPath, mounts) => {
406
+ let match = null;
407
+ for (const mount of mounts) {
408
+ if (!pathStartsWith(hostPath, mount.destination)) {
409
+ continue;
410
+ }
411
+ if (match === null || mount.destination.length > match.destination.length) {
412
+ match = mount;
413
+ }
414
+ }
415
+ if (match === null) {
416
+ return hostPath;
417
+ }
418
+ return `${match.source}${hostPath.slice(match.destination.length)}`;
419
+ };
420
+ const resolveDockerVolumeHostPath = (cwd, hostPath) => Effect.gen(function* (_) {
421
+ const containerProjectsRoot = resolveContainerProjectsRoot();
422
+ const hostProjectsRoot = resolveProjectsRootHostOverride();
423
+ if (containerProjectsRoot !== null && hostProjectsRoot !== null) {
424
+ const remapped = translatePathPrefix(hostPath, containerProjectsRoot, hostProjectsRoot);
425
+ if (remapped !== null) {
426
+ return remapped;
427
+ }
428
+ }
429
+ const containerId = yield* _(resolveCurrentContainerId(cwd));
430
+ if (containerId === null) {
431
+ return hostPath;
432
+ }
433
+ const mountsJson = yield* _(
434
+ runCommandCapture(
435
+ {
436
+ cwd,
437
+ command: "docker",
438
+ args: [
439
+ "inspect",
440
+ containerId,
441
+ "--format",
442
+ String.raw`{{range .Mounts}}{{println .Source "\t" .Destination}}{{end}}`
443
+ ]
444
+ },
445
+ [0],
446
+ () => new Error("docker inspect current container failed")
447
+ ).pipe(Effect.orElseSucceed(() => ""))
448
+ );
449
+ return remapDockerBindHostPathFromMounts(hostPath, parseDockerInspectMounts(mountsJson));
450
+ });
451
+ const resolveDefaultDockerUser = () => {
452
+ const getUid = Reflect.get(process, "getuid");
453
+ const getGid = Reflect.get(process, "getgid");
454
+ if (typeof getUid !== "function" || typeof getGid !== "function") {
455
+ return null;
456
+ }
457
+ const uid = getUid.call(process);
458
+ const gid = getGid.call(process);
459
+ if (typeof uid !== "number" || typeof gid !== "number") {
460
+ return null;
461
+ }
462
+ return `${uid}:${gid}`;
463
+ };
464
+ const appendEnvArgs = (base, env) => {
465
+ if (typeof env === "string") {
466
+ const trimmed = env.trim();
467
+ if (trimmed.length > 0) {
468
+ base.push("-e", trimmed);
469
+ }
470
+ return;
471
+ }
472
+ for (const entry of env) {
473
+ const trimmed = entry.trim();
474
+ if (trimmed.length === 0) {
475
+ continue;
476
+ }
477
+ base.push("-e", trimmed);
478
+ }
479
+ };
480
+ const buildDockerArgs = (spec) => {
481
+ const base = ["run", "--rm"];
482
+ const dockerUser = (spec.user ?? "").trim() || resolveDefaultDockerUser();
483
+ if (dockerUser !== null) {
484
+ base.push("--user", dockerUser);
485
+ }
486
+ if (spec.interactive) {
487
+ base.push("-it");
488
+ }
489
+ if (spec.entrypoint && spec.entrypoint.length > 0) {
490
+ base.push("--entrypoint", spec.entrypoint);
491
+ }
492
+ base.push("-v", `${spec.volume.hostPath}:${spec.volume.containerPath}`);
493
+ if (spec.env !== void 0) {
494
+ appendEnvArgs(base, spec.env);
495
+ }
496
+ return [...base, spec.image, ...spec.args];
497
+ };
498
+ const runDockerAuth = (spec, okExitCodes, onFailure) => Effect.gen(function* (_) {
499
+ const hostPath = yield* _(resolveDockerVolumeHostPath(spec.cwd, spec.volume.hostPath));
500
+ yield* _(
501
+ runCommandWithExitCodes(
502
+ {
503
+ cwd: spec.cwd,
504
+ command: "docker",
505
+ args: buildDockerArgs({ ...spec, volume: { ...spec.volume, hostPath } })
506
+ },
507
+ okExitCodes,
508
+ onFailure
509
+ )
510
+ );
511
+ });
512
+ const runDockerAuthCapture = (spec, okExitCodes, onFailure) => Effect.gen(function* (_) {
513
+ const hostPath = yield* _(resolveDockerVolumeHostPath(spec.cwd, spec.volume.hostPath));
514
+ return yield* _(
515
+ runCommandCapture(
516
+ {
517
+ cwd: spec.cwd,
518
+ command: "docker",
519
+ args: buildDockerArgs({ ...spec, volume: { ...spec.volume, hostPath } })
520
+ },
521
+ okExitCodes,
522
+ onFailure
523
+ )
524
+ );
525
+ });
526
+ const runDockerAuthExitCode = (spec) => Effect.gen(function* (_) {
527
+ const hostPath = yield* _(resolveDockerVolumeHostPath(spec.cwd, spec.volume.hostPath));
528
+ return yield* _(
529
+ runCommandExitCode({
530
+ cwd: spec.cwd,
531
+ command: "docker",
532
+ args: buildDockerArgs({ ...spec, volume: { ...spec.volume, hostPath } })
533
+ })
534
+ );
535
+ });
536
+ const composeSpec = (cwd, args) => ({
537
+ cwd,
538
+ command: "docker",
539
+ args: ["compose", "--ansi", "never", "--progress", "plain", ...args]
540
+ });
541
+ const resolveProjectsRootCandidate = () => {
542
+ const explicit = resolveDockerEnvValue("DOCKER_GIT_PROJECTS_ROOT");
543
+ if (explicit !== null) {
544
+ return explicit;
545
+ }
546
+ const home = resolveDockerEnvValue("HOME") ?? resolveDockerEnvValue("USERPROFILE");
547
+ return home === null ? null : `${trimDockerPathTrailingSlash(home)}/.docker-git`;
548
+ };
549
+ const resolveDockerComposeEnv = (cwd) => Effect.gen(function* (_) {
550
+ const projectsRoot = resolveProjectsRootCandidate();
551
+ if (projectsRoot === null) {
552
+ return {};
553
+ }
554
+ const remappedProjectsRoot = yield* _(resolveDockerVolumeHostPath(cwd, projectsRoot));
555
+ return remappedProjectsRoot === projectsRoot ? {} : { DOCKER_GIT_PROJECTS_ROOT_HOST: remappedProjectsRoot };
556
+ });
557
+ const parseInspectNetworkEntry = (line) => {
558
+ const idx = line.indexOf("=");
559
+ if (idx <= 0) {
560
+ return [];
561
+ }
562
+ const network = line.slice(0, idx).trim();
563
+ const ip = line.slice(idx + 1).trim();
564
+ if (network.length === 0 || ip.length === 0) {
565
+ return [];
566
+ }
567
+ const entry = [network, ip];
568
+ return [entry];
569
+ };
348
570
  class FileExistsError extends Data.TaggedError("FileExistsError") {
349
571
  }
350
572
  class ConfigNotFoundError extends Data.TaggedError("ConfigNotFoundError") {
@@ -513,34 +735,32 @@ const runDockerPsPublishedHostPorts = (cwd) => pipe(
513
735
  ),
514
736
  Effect.map((output) => parseDockerPublishedHostPorts(output))
515
737
  );
516
- const composeSpec = (cwd, args) => ({
517
- cwd,
518
- command: "docker",
519
- args: ["compose", "--ansi", "never", "--progress", "plain", ...args]
738
+ const runCompose = (cwd, args, okExitCodes) => Effect.gen(function* (_) {
739
+ const env = yield* _(resolveDockerComposeEnv(cwd));
740
+ yield* _(
741
+ runCommandWithExitCodes(
742
+ {
743
+ ...composeSpec(cwd, args),
744
+ ...Object.keys(env).length > 0 ? { env } : {}
745
+ },
746
+ okExitCodes,
747
+ (exitCode) => new DockerCommandError({ exitCode })
748
+ )
749
+ );
750
+ });
751
+ const runComposeCapture = (cwd, args, okExitCodes) => Effect.gen(function* (_) {
752
+ const env = yield* _(resolveDockerComposeEnv(cwd));
753
+ return yield* _(
754
+ runCommandCapture(
755
+ {
756
+ ...composeSpec(cwd, args),
757
+ ...Object.keys(env).length > 0 ? { env } : {}
758
+ },
759
+ okExitCodes,
760
+ (exitCode) => new DockerCommandError({ exitCode })
761
+ )
762
+ );
520
763
  });
521
- const parseInspectNetworkEntry = (line) => {
522
- const idx = line.indexOf("=");
523
- if (idx <= 0) {
524
- return [];
525
- }
526
- const network = line.slice(0, idx).trim();
527
- const ip = line.slice(idx + 1).trim();
528
- if (network.length === 0 || ip.length === 0) {
529
- return [];
530
- }
531
- const entry = [network, ip];
532
- return [entry];
533
- };
534
- const runCompose = (cwd, args, okExitCodes) => runCommandWithExitCodes(
535
- composeSpec(cwd, args),
536
- okExitCodes,
537
- (exitCode) => new DockerCommandError({ exitCode })
538
- );
539
- const runComposeCapture = (cwd, args, okExitCodes) => runCommandCapture(
540
- composeSpec(cwd, args),
541
- okExitCodes,
542
- (exitCode) => new DockerCommandError({ exitCode })
543
- );
544
764
  const dockerComposeUpRetrySchedule = Schedule.addDelay(
545
765
  Schedule.recurs(2),
546
766
  () => Duration.seconds(2)
@@ -751,313 +971,291 @@ const logDockerAccessInfo = (cwd, config) => ensureDockerDnsHost(cwd, config.con
751
971
  Effect.zipRight(logDockerDnsAccess(config)),
752
972
  Effect.zipRight(logContainerIpAccess(cwd, config))
753
973
  );
754
- const formatParseError$1 = (error) => Match.value(error).pipe(
755
- Match.when({ _tag: "UnknownCommand" }, ({ command }) => `Unknown command: ${command}`),
756
- Match.when({ _tag: "UnknownOption" }, ({ option }) => `Unknown option: ${option}`),
757
- Match.when({ _tag: "MissingOptionValue" }, ({ option }) => `Missing value for option: ${option}`),
758
- Match.when({ _tag: "MissingRequiredOption" }, ({ option }) => `Missing required option: ${option}`),
759
- Match.when({ _tag: "InvalidOption" }, ({ option, reason }) => `Invalid option ${option}: ${reason}`),
760
- Match.when({ _tag: "UnexpectedArgument" }, ({ value }) => `Unexpected argument: ${value}`),
761
- Match.exhaustive
762
- );
763
- const isParseError$1 = (error) => error._tag === "UnknownCommand" || error._tag === "UnknownOption" || error._tag === "MissingOptionValue" || error._tag === "MissingRequiredOption" || error._tag === "InvalidOption" || error._tag === "UnexpectedArgument";
764
- const renderDockerAccessHeadline = (issue) => issue === "PermissionDenied" ? "Cannot access Docker daemon socket: permission denied." : "Cannot connect to Docker daemon.";
765
- const renderDockerAccessActionPlan = (issue) => {
766
- const permissionDeniedPlan = [
767
- "Action plan:",
768
- "1) In the same shell, run: `groups $USER` and make sure group `docker` is present.",
769
- "2) Re-login to refresh group memberships and run command again.",
770
- "3) If DOCKER_HOST is set to rootless socket, keep running: `export DOCKER_HOST=unix:///run/user/$UID/docker.sock`.",
771
- "4) If using a dedicated socket not in /run/user, set DOCKER_HOST explicitly and re-run.",
772
- "Tip: this app now auto-tries a rootless socket fallback on first permission error."
773
- ];
774
- const daemonUnavailablePlan = [
775
- "Action plan:",
776
- "1) Check daemon status: `systemctl --user status docker` or `systemctl status docker`.",
777
- "2) Start daemon: `systemctl --user start docker` (or `systemctl start docker` for system Docker).",
778
- "3) Retry command in a new shell."
779
- ];
780
- return issue === "PermissionDenied" ? permissionDeniedPlan.join("\n") : daemonUnavailablePlan.join("\n");
974
+ const normalizeAccountLabel = (value, fallback) => {
975
+ const trimmed = value?.trim() ?? "";
976
+ if (trimmed.length === 0) {
977
+ return fallback;
978
+ }
979
+ const normalized = trimmed.toLowerCase().replaceAll(/[^a-z0-9]+/g, "-");
980
+ const withoutLeading = trimLeftChar(normalized, "-");
981
+ const cleaned = trimRightChar(withoutLeading, "-");
982
+ return cleaned.length > 0 ? cleaned : fallback;
781
983
  };
782
- const renderDockerCommandError = ({ exitCode }) => [
783
- `docker compose failed with exit code ${exitCode}`,
784
- "Hint: ensure Docker daemon is running and current user can access /var/run/docker.sock (for example via the docker group).",
785
- "Hint: if output above contains 'port is already allocated', retry with a free SSH port via --ssh-port <port> (for example --ssh-port 2235), or stop the conflicting project/container.",
786
- "Hint: if output above contains 'all predefined address pools have been fully subnetted', run `docker network prune -f`, configure Docker `default-address-pools`, or use shared network mode (`--network-mode shared`).",
787
- "Hint: if output above contains 'lookup auth.docker.io' or 'read udp ... [::1]:53 ... connection refused', fix Docker DNS resolver (set working DNS in host/daemon config) and retry."
788
- ].join("\n");
789
- const renderDockerAccessError = ({ details, issue }) => [
790
- renderDockerAccessHeadline(issue),
791
- "Hint: ensure Docker daemon is running and current user can access the docker socket.",
792
- "Hint: if you use rootless Docker, set DOCKER_HOST to your user socket (for example unix:///run/user/$UID/docker.sock).",
793
- renderDockerAccessActionPlan(issue),
794
- `Details: ${details}`
795
- ].join("\n");
796
- const renderPrimaryError = (error) => Match.value(error).pipe(
797
- Match.when({ _tag: "FileExistsError" }, ({ path }) => `File already exists: ${path} (use --force to overwrite)`),
798
- Match.when({ _tag: "DockerCommandError" }, renderDockerCommandError),
799
- Match.when({ _tag: "DockerAccessError" }, renderDockerAccessError),
800
- Match.when({ _tag: "CloneFailedError" }, ({ repoRef, repoUrl, targetDir }) => `Clone failed for ${repoUrl} (${repoRef}) into ${targetDir}`),
801
- Match.when({ _tag: "AgentFailedError" }, ({ agentMode, targetDir }) => `Agent (${agentMode}) failed in ${targetDir}`),
802
- Match.when({ _tag: "PortProbeError" }, ({ message, port }) => `SSH port check failed for ${port}: ${message}`),
803
- Match.when(
804
- { _tag: "CommandFailedError" },
805
- ({ command, exitCode }) => `${command} failed with exit code ${exitCode}`
806
- ),
807
- Match.when(
808
- { _tag: "ScrapArchiveNotFoundError" },
809
- ({ path }) => `Scrap archive not found: ${path} (run docker-git scrap export first)`
810
- ),
811
- Match.when(
812
- { _tag: "ScrapArchiveInvalidError" },
813
- ({ message, path }) => `Invalid scrap archive: ${path}
814
- Details: ${message}`
815
- ),
816
- Match.when({ _tag: "ScrapTargetDirUnsupportedError" }, ({ reason, targetDir }) => [
817
- `Cannot use scrap with targetDir ${targetDir}.`,
818
- `Reason: ${reason}`,
819
- `Hint: scrap currently supports workspaces under the ssh home directory only (for example: ~/repo).`
820
- ].join("\n")),
821
- Match.when({ _tag: "ScrapWipeRefusedError" }, ({ reason, targetDir }) => [
822
- `Refusing to wipe workspace for scrap import (targetDir ${targetDir}).`,
823
- `Reason: ${reason}`,
824
- "Hint: re-run with --no-wipe, or set a narrower --target-dir when creating the project."
825
- ].join("\n")),
826
- Match.when({ _tag: "AuthError" }, ({ message }) => message),
827
- Match.orElse(() => null)
984
+ const buildDockerAuthSpec = (input) => ({
985
+ cwd: input.cwd,
986
+ image: input.image,
987
+ volume: { hostPath: input.hostPath, containerPath: input.containerPath },
988
+ ...typeof input.entrypoint === "string" ? { entrypoint: input.entrypoint } : {},
989
+ ...input.env === void 0 ? {} : { env: input.env },
990
+ args: input.args,
991
+ interactive: input.interactive
992
+ });
993
+ const JsonValueSchema = Schema.suspend(
994
+ () => Schema.Union(
995
+ Schema.Null,
996
+ Schema.Boolean,
997
+ Schema.String,
998
+ Schema.JsonNumber,
999
+ Schema.Array(JsonValueSchema),
1000
+ Schema.Record({ key: Schema.String, value: JsonValueSchema })
1001
+ )
828
1002
  );
829
- const renderConfigError = (error) => {
830
- if (error._tag === "ConfigNotFoundError") {
831
- return `docker-git.json not found: ${error.path} (run docker-git create in that directory)`;
1003
+ const JsonRecordSchema = Schema.Record({
1004
+ key: Schema.String,
1005
+ value: JsonValueSchema
1006
+ });
1007
+ const JsonRecordFromStringSchema = Schema.parseJson(JsonRecordSchema);
1008
+ const defaultEnvContents$1 = "# docker-git env\n# KEY=value\n";
1009
+ const codexConfigMarker = "# docker-git codex config";
1010
+ const defaultCodexConfig = [
1011
+ "# docker-git codex config",
1012
+ 'model = "gpt-5.4"',
1013
+ "model_context_window = 1050000",
1014
+ "model_auto_compact_token_limit = 945000",
1015
+ 'model_reasoning_effort = "xhigh"',
1016
+ 'plan_mode_reasoning_effort = "xhigh"',
1017
+ 'personality = "pragmatic"',
1018
+ "",
1019
+ 'approval_policy = "never"',
1020
+ 'sandbox_mode = "danger-full-access"',
1021
+ 'web_search = "live"',
1022
+ "",
1023
+ "[features]",
1024
+ "shell_snapshot = true",
1025
+ "multi_agent = true",
1026
+ "apps = true",
1027
+ "shell_tool = true"
1028
+ ].join("\n");
1029
+ const resolvePathFromBase$1 = (path, baseDir, targetPath) => path.isAbsolute(targetPath) ? targetPath : path.resolve(baseDir, targetPath);
1030
+ const isPermissionDeniedSystemError = (error) => error._tag === "SystemError" && error.reason === "PermissionDenied";
1031
+ const skipCodexConfigPermissionDenied = (configPath, error) => isPermissionDeniedSystemError(error) ? Effect.logWarning(
1032
+ `Skipped Codex config sync at ${configPath}: permission denied (${error.description ?? "no details"}).`
1033
+ ) : Effect.fail(error);
1034
+ const normalizeConfigText = (text) => text.replaceAll("\r\n", "\n").trim();
1035
+ const shouldRewriteDockerGitCodexConfig = (existing) => {
1036
+ const normalized = normalizeConfigText(existing);
1037
+ if (normalized.length === 0) {
1038
+ return true;
832
1039
  }
833
- if (error._tag === "ConfigDecodeError") {
834
- return `Invalid docker-git.json at ${error.path}: ${error.message}`;
1040
+ if (!normalized.startsWith(codexConfigMarker)) {
1041
+ return false;
835
1042
  }
836
- return null;
1043
+ return normalized !== normalizeConfigText(defaultCodexConfig);
837
1044
  };
838
- const renderInputError = (error) => {
839
- if (error._tag === "InputCancelledError") {
840
- return "Input cancelled.";
1045
+ const shouldCopyEnv = (sourceText, targetText) => {
1046
+ if (sourceText.trim().length === 0) {
1047
+ return "skip";
841
1048
  }
842
- if (error._tag === "InputReadError") {
843
- return `Input error: ${error.message}`;
1049
+ if (targetText.trim().length === 0) {
1050
+ return "copy";
844
1051
  }
845
- return null;
846
- };
847
- const renderNonParseError = (error) => renderPrimaryError(error) ?? renderConfigError(error) ?? renderInputError(error) ?? error.message;
848
- const renderError = (error) => {
849
- if (isParseError$1(error)) {
850
- return formatParseError$1(error);
1052
+ if (targetText.trim() === defaultEnvContents$1.trim() && sourceText.trim() !== defaultEnvContents$1.trim()) {
1053
+ return "copy";
851
1054
  }
852
- return renderNonParseError(error);
853
- };
854
- const resolveEnvValue = (key) => {
855
- const value = process.env[key]?.trim();
856
- return value && value.length > 0 ? value : null;
1055
+ return "skip";
857
1056
  };
858
- const trimTrailingSlash$1 = (value) => {
859
- let end = value.length;
860
- while (end > 0) {
861
- const char = value[end - 1];
862
- if (char !== "/" && char !== "\\") {
863
- break;
864
- }
865
- end -= 1;
1057
+ const parseJsonRecord = (text) => Either.match(ParseResult.decodeUnknownEither(JsonRecordFromStringSchema)(text), {
1058
+ onLeft: () => Effect.succeed(null),
1059
+ onRight: (record) => Effect.succeed(record)
1060
+ });
1061
+ const hasClaudeOauthAccount = (record) => record !== null && typeof record["oauthAccount"] === "object" && record["oauthAccount"] !== null;
1062
+ const hasClaudeCredentials = (record) => record !== null && typeof record["claudeAiOauth"] === "object" && record["claudeAiOauth"] !== null;
1063
+ const isGithubTokenKey = (key) => key === "GITHUB_TOKEN" || key === "GH_TOKEN" || key.startsWith("GITHUB_TOKEN__");
1064
+ const hasNonEmptyFile = (fs, filePath) => Effect.gen(function* (_) {
1065
+ const exists = yield* _(fs.exists(filePath));
1066
+ if (!exists) {
1067
+ return false;
866
1068
  }
867
- return value.slice(0, end);
868
- };
869
- const pathStartsWith = (candidate, prefix) => candidate === prefix || candidate.startsWith(`${prefix}/`) || candidate.startsWith(`${prefix}\\`);
870
- const translatePathPrefix = (candidate, sourcePrefix, targetPrefix) => pathStartsWith(candidate, sourcePrefix) ? `${targetPrefix}${candidate.slice(sourcePrefix.length)}` : null;
871
- const resolveContainerProjectsRoot = () => {
872
- const explicit = resolveEnvValue("DOCKER_GIT_PROJECTS_ROOT");
873
- if (explicit !== null) {
874
- return explicit;
1069
+ const info = yield* _(fs.stat(filePath));
1070
+ if (info.type !== "File") {
1071
+ return false;
875
1072
  }
876
- const home = resolveEnvValue("HOME") ?? resolveEnvValue("USERPROFILE");
877
- return home === null ? null : `${trimTrailingSlash$1(home)}/.docker-git`;
878
- };
879
- const resolveProjectsRootHostOverride = () => resolveEnvValue("DOCKER_GIT_PROJECTS_ROOT_HOST");
880
- const resolveCurrentContainerId = (cwd) => {
881
- const fromEnv = resolveEnvValue("HOSTNAME");
882
- if (fromEnv !== null) {
883
- return Effect.succeed(fromEnv);
1073
+ const text = yield* _(fs.readFileString(filePath), Effect.orElseSucceed(() => ""));
1074
+ return text.trim().length > 0;
1075
+ });
1076
+ const autoOptionError = (reason) => ({
1077
+ _tag: "InvalidOption",
1078
+ option: "--auto",
1079
+ reason
1080
+ });
1081
+ const isRegularFile$1 = (fs, filePath) => Effect.gen(function* (_) {
1082
+ const exists = yield* _(fs.exists(filePath));
1083
+ if (!exists) {
1084
+ return false;
884
1085
  }
885
- return runCommandCapture(
886
- {
887
- cwd,
888
- command: "hostname",
889
- args: []
890
- },
891
- [0],
892
- () => new Error("hostname failed")
893
- ).pipe(
894
- Effect.map((value) => value.trim()),
895
- Effect.orElseSucceed(() => ""),
896
- Effect.map((value) => value.length > 0 ? value : null)
897
- );
1086
+ const info = yield* _(fs.stat(filePath));
1087
+ return info.type === "File";
1088
+ });
1089
+ const hasCodexAuth = (fs, rootPath, label) => {
1090
+ const normalized = normalizeAccountLabel(label ?? null, "default");
1091
+ const authPath = normalized === "default" ? `${rootPath}/auth.json` : `${rootPath}/${normalized}/auth.json`;
1092
+ return hasNonEmptyFile(fs, authPath);
898
1093
  };
899
- const parseDockerInspectMounts = (raw) => raw.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0).flatMap((line) => {
900
- const separator = line.indexOf(" ");
901
- if (separator <= 0 || separator >= line.length - 1) {
902
- return [];
903
- }
904
- const source = line.slice(0, separator).trim();
905
- const destination = line.slice(separator + 1).trim();
906
- if (source.length === 0 || destination.length === 0) {
907
- return [];
1094
+ const resolveClaudeAccountPath$1 = (rootPath, label) => {
1095
+ const normalized = normalizeAccountLabel(label ?? null, "default");
1096
+ if (normalized !== "default") {
1097
+ return [`${rootPath}/${normalized}`];
908
1098
  }
909
- return [{ source, destination }];
910
- });
911
- const remapDockerBindHostPathFromMounts = (hostPath, mounts) => {
912
- let match = null;
913
- for (const mount of mounts) {
914
- if (!pathStartsWith(hostPath, mount.destination)) {
915
- continue;
1099
+ return [rootPath, `${rootPath}/default`];
1100
+ };
1101
+ const hasClaudeAuth = (fs, rootPath, label) => Effect.gen(function* (_) {
1102
+ for (const accountPath of resolveClaudeAccountPath$1(rootPath, label)) {
1103
+ const oauthToken = yield* _(hasNonEmptyFile(fs, `${accountPath}/.oauth-token`));
1104
+ if (oauthToken) {
1105
+ return true;
916
1106
  }
917
- if (match === null || mount.destination.length > match.destination.length) {
918
- match = mount;
1107
+ const credentials = yield* _(isRegularFile$1(fs, `${accountPath}/.credentials.json`));
1108
+ if (credentials) {
1109
+ return true;
919
1110
  }
920
- }
921
- if (match === null) {
922
- return hostPath;
923
- }
924
- return `${match.source}${hostPath.slice(match.destination.length)}`;
925
- };
926
- const resolveDockerVolumeHostPath = (cwd, hostPath) => Effect.gen(function* (_) {
927
- const containerProjectsRoot = resolveContainerProjectsRoot();
928
- const hostProjectsRoot = resolveProjectsRootHostOverride();
929
- if (containerProjectsRoot !== null && hostProjectsRoot !== null) {
930
- const remapped = translatePathPrefix(hostPath, containerProjectsRoot, hostProjectsRoot);
931
- if (remapped !== null) {
932
- return remapped;
1111
+ const nestedCredentials = yield* _(isRegularFile$1(fs, `${accountPath}/.claude/.credentials.json`));
1112
+ if (nestedCredentials) {
1113
+ return true;
933
1114
  }
934
1115
  }
935
- const containerId = yield* _(resolveCurrentContainerId(cwd));
936
- if (containerId === null) {
937
- return hostPath;
938
- }
939
- const mountsJson = yield* _(
940
- runCommandCapture(
941
- {
942
- cwd,
943
- command: "docker",
944
- args: [
945
- "inspect",
946
- containerId,
947
- "--format",
948
- String.raw`{{range .Mounts}}{{println .Source "\t" .Destination}}{{end}}`
949
- ]
950
- },
951
- [0],
952
- () => new Error("docker inspect current container failed")
953
- ).pipe(Effect.orElseSucceed(() => ""))
1116
+ return false;
1117
+ });
1118
+ const resolveClaudeRoot = (codexSharedAuthPath) => `${codexSharedAuthPath.slice(0, codexSharedAuthPath.lastIndexOf("/"))}/claude`;
1119
+ const resolveAvailableAgentAuth = (fs, config) => Effect.gen(function* (_) {
1120
+ const claudeAvailable = yield* _(
1121
+ hasClaudeAuth(fs, resolveClaudeRoot(config.codexSharedAuthPath), config.claudeAuthLabel)
954
1122
  );
955
- return remapDockerBindHostPathFromMounts(hostPath, parseDockerInspectMounts(mountsJson));
1123
+ const codexAvailable = yield* _(hasCodexAuth(fs, config.codexSharedAuthPath, config.codexAuthLabel));
1124
+ return { claudeAvailable, codexAvailable };
956
1125
  });
957
- const resolveDefaultDockerUser = () => {
958
- const getUid = Reflect.get(process, "getuid");
959
- const getGid = Reflect.get(process, "getgid");
960
- if (typeof getUid !== "function" || typeof getGid !== "function") {
961
- return null;
1126
+ const resolveExplicitAutoAgentMode = (available, mode) => {
1127
+ if (mode === "claude") {
1128
+ return available.claudeAvailable ? Effect.succeed("claude") : Effect.fail(autoOptionError("Claude auth not found"));
962
1129
  }
963
- const uid = getUid.call(process);
964
- const gid = getGid.call(process);
965
- if (typeof uid !== "number" || typeof gid !== "number") {
966
- return null;
1130
+ if (mode === "codex") {
1131
+ return available.codexAvailable ? Effect.succeed("codex") : Effect.fail(autoOptionError("Codex auth not found"));
967
1132
  }
968
- return `${uid}:${gid}`;
1133
+ return Effect.sync(() => mode);
969
1134
  };
970
- const appendEnvArgs = (base, env) => {
971
- if (typeof env === "string") {
972
- const trimmed = env.trim();
973
- if (trimmed.length > 0) {
974
- base.push("-e", trimmed);
975
- }
976
- return;
1135
+ const pickRandomAutoAgentMode = (available) => {
1136
+ if (!available.claudeAvailable && !available.codexAvailable) {
1137
+ return Effect.fail(autoOptionError("no Claude or Codex auth found"));
977
1138
  }
978
- for (const entry of env) {
979
- const trimmed = entry.trim();
980
- if (trimmed.length === 0) {
981
- continue;
982
- }
983
- base.push("-e", trimmed);
1139
+ if (available.claudeAvailable && !available.codexAvailable) {
1140
+ return Effect.succeed("claude");
1141
+ }
1142
+ if (!available.claudeAvailable && available.codexAvailable) {
1143
+ return Effect.succeed("codex");
1144
+ }
1145
+ return Effect.sync(() => process.hrtime.bigint() % 2n === 0n ? "claude" : "codex");
1146
+ };
1147
+ const resolveAutoAgentMode = (config) => Effect.gen(function* (_) {
1148
+ const fs = yield* _(FileSystem.FileSystem);
1149
+ if (config.agentAuto !== true) {
1150
+ return config.agentMode;
1151
+ }
1152
+ const available = yield* _(resolveAvailableAgentAuth(fs, config));
1153
+ const explicitMode = yield* _(resolveExplicitAutoAgentMode(available, config.agentMode));
1154
+ if (explicitMode !== void 0) {
1155
+ return explicitMode;
984
1156
  }
1157
+ return yield* _(pickRandomAutoAgentMode(available));
1158
+ });
1159
+ const formatParseError$1 = (error) => Match.value(error).pipe(
1160
+ Match.when({ _tag: "UnknownCommand" }, ({ command }) => `Unknown command: ${command}`),
1161
+ Match.when({ _tag: "UnknownOption" }, ({ option }) => `Unknown option: ${option}`),
1162
+ Match.when({ _tag: "MissingOptionValue" }, ({ option }) => `Missing value for option: ${option}`),
1163
+ Match.when({ _tag: "MissingRequiredOption" }, ({ option }) => `Missing required option: ${option}`),
1164
+ Match.when({ _tag: "InvalidOption" }, ({ option, reason }) => `Invalid option ${option}: ${reason}`),
1165
+ Match.when({ _tag: "UnexpectedArgument" }, ({ value }) => `Unexpected argument: ${value}`),
1166
+ Match.exhaustive
1167
+ );
1168
+ const isParseError$1 = (error) => error._tag === "UnknownCommand" || error._tag === "UnknownOption" || error._tag === "MissingOptionValue" || error._tag === "MissingRequiredOption" || error._tag === "InvalidOption" || error._tag === "UnexpectedArgument";
1169
+ const renderDockerAccessHeadline = (issue) => issue === "PermissionDenied" ? "Cannot access Docker daemon socket: permission denied." : "Cannot connect to Docker daemon.";
1170
+ const renderDockerAccessActionPlan = (issue) => {
1171
+ const permissionDeniedPlan = [
1172
+ "Action plan:",
1173
+ "1) In the same shell, run: `groups $USER` and make sure group `docker` is present.",
1174
+ "2) Re-login to refresh group memberships and run command again.",
1175
+ "3) If DOCKER_HOST is set to rootless socket, keep running: `export DOCKER_HOST=unix:///run/user/$UID/docker.sock`.",
1176
+ "4) If using a dedicated socket not in /run/user, set DOCKER_HOST explicitly and re-run.",
1177
+ "Tip: this app now auto-tries a rootless socket fallback on first permission error."
1178
+ ];
1179
+ const daemonUnavailablePlan = [
1180
+ "Action plan:",
1181
+ "1) Check daemon status: `systemctl --user status docker` or `systemctl status docker`.",
1182
+ "2) Start daemon: `systemctl --user start docker` (or `systemctl start docker` for system Docker).",
1183
+ "3) Retry command in a new shell."
1184
+ ];
1185
+ return issue === "PermissionDenied" ? permissionDeniedPlan.join("\n") : daemonUnavailablePlan.join("\n");
985
1186
  };
986
- const buildDockerArgs = (spec) => {
987
- const base = ["run", "--rm"];
988
- const dockerUser = (spec.user ?? "").trim() || resolveDefaultDockerUser();
989
- if (dockerUser !== null) {
990
- base.push("--user", dockerUser);
1187
+ const renderDockerCommandError = ({ exitCode }) => [
1188
+ `docker compose failed with exit code ${exitCode}`,
1189
+ "Hint: ensure Docker daemon is running and current user can access /var/run/docker.sock (for example via the docker group).",
1190
+ "Hint: if output above contains 'port is already allocated', retry with a free SSH port via --ssh-port <port> (for example --ssh-port 2235), or stop the conflicting project/container.",
1191
+ "Hint: if output above contains 'all predefined address pools have been fully subnetted', run `docker network prune -f`, configure Docker `default-address-pools`, or use shared network mode (`--network-mode shared`).",
1192
+ "Hint: if output above contains 'lookup auth.docker.io' or 'read udp ... [::1]:53 ... connection refused', fix Docker DNS resolver (set working DNS in host/daemon config) and retry."
1193
+ ].join("\n");
1194
+ const renderDockerAccessError = ({ details, issue }) => [
1195
+ renderDockerAccessHeadline(issue),
1196
+ "Hint: ensure Docker daemon is running and current user can access the docker socket.",
1197
+ "Hint: if you use rootless Docker, set DOCKER_HOST to your user socket (for example unix:///run/user/$UID/docker.sock).",
1198
+ renderDockerAccessActionPlan(issue),
1199
+ `Details: ${details}`
1200
+ ].join("\n");
1201
+ const renderPrimaryError = (error) => Match.value(error).pipe(
1202
+ Match.when({ _tag: "FileExistsError" }, ({ path }) => `File already exists: ${path} (use --force to overwrite)`),
1203
+ Match.when({ _tag: "DockerCommandError" }, renderDockerCommandError),
1204
+ Match.when({ _tag: "DockerAccessError" }, renderDockerAccessError),
1205
+ Match.when({ _tag: "CloneFailedError" }, ({ repoRef, repoUrl, targetDir }) => `Clone failed for ${repoUrl} (${repoRef}) into ${targetDir}`),
1206
+ Match.when({ _tag: "AgentFailedError" }, ({ agentMode, targetDir }) => `Agent (${agentMode}) failed in ${targetDir}`),
1207
+ Match.when({ _tag: "PortProbeError" }, ({ message, port }) => `SSH port check failed for ${port}: ${message}`),
1208
+ Match.when(
1209
+ { _tag: "CommandFailedError" },
1210
+ ({ command, exitCode }) => `${command} failed with exit code ${exitCode}`
1211
+ ),
1212
+ Match.when(
1213
+ { _tag: "ScrapArchiveNotFoundError" },
1214
+ ({ path }) => `Scrap archive not found: ${path} (run docker-git scrap export first)`
1215
+ ),
1216
+ Match.when(
1217
+ { _tag: "ScrapArchiveInvalidError" },
1218
+ ({ message, path }) => `Invalid scrap archive: ${path}
1219
+ Details: ${message}`
1220
+ ),
1221
+ Match.when({ _tag: "ScrapTargetDirUnsupportedError" }, ({ reason, targetDir }) => [
1222
+ `Cannot use scrap with targetDir ${targetDir}.`,
1223
+ `Reason: ${reason}`,
1224
+ `Hint: scrap currently supports workspaces under the ssh home directory only (for example: ~/repo).`
1225
+ ].join("\n")),
1226
+ Match.when({ _tag: "ScrapWipeRefusedError" }, ({ reason, targetDir }) => [
1227
+ `Refusing to wipe workspace for scrap import (targetDir ${targetDir}).`,
1228
+ `Reason: ${reason}`,
1229
+ "Hint: re-run with --no-wipe, or set a narrower --target-dir when creating the project."
1230
+ ].join("\n")),
1231
+ Match.when({ _tag: "AuthError" }, ({ message }) => message),
1232
+ Match.orElse(() => null)
1233
+ );
1234
+ const renderConfigError = (error) => {
1235
+ if (error._tag === "ConfigNotFoundError") {
1236
+ return `docker-git.json not found: ${error.path} (run docker-git create in that directory)`;
991
1237
  }
992
- if (spec.interactive) {
993
- base.push("-it");
1238
+ if (error._tag === "ConfigDecodeError") {
1239
+ return `Invalid docker-git.json at ${error.path}: ${error.message}`;
994
1240
  }
995
- if (spec.entrypoint && spec.entrypoint.length > 0) {
996
- base.push("--entrypoint", spec.entrypoint);
1241
+ return null;
1242
+ };
1243
+ const renderInputError = (error) => {
1244
+ if (error._tag === "InputCancelledError") {
1245
+ return "Input cancelled.";
997
1246
  }
998
- base.push("-v", `${spec.volume.hostPath}:${spec.volume.containerPath}`);
999
- if (spec.env !== void 0) {
1000
- appendEnvArgs(base, spec.env);
1247
+ if (error._tag === "InputReadError") {
1248
+ return `Input error: ${error.message}`;
1001
1249
  }
1002
- return [...base, spec.image, ...spec.args];
1250
+ return null;
1003
1251
  };
1004
- const runDockerAuth = (spec, okExitCodes, onFailure) => Effect.gen(function* (_) {
1005
- const hostPath = yield* _(resolveDockerVolumeHostPath(spec.cwd, spec.volume.hostPath));
1006
- yield* _(
1007
- runCommandWithExitCodes(
1008
- {
1009
- cwd: spec.cwd,
1010
- command: "docker",
1011
- args: buildDockerArgs({ ...spec, volume: { ...spec.volume, hostPath } })
1012
- },
1013
- okExitCodes,
1014
- onFailure
1015
- )
1016
- );
1017
- });
1018
- const runDockerAuthCapture = (spec, okExitCodes, onFailure) => Effect.gen(function* (_) {
1019
- const hostPath = yield* _(resolveDockerVolumeHostPath(spec.cwd, spec.volume.hostPath));
1020
- return yield* _(
1021
- runCommandCapture(
1022
- {
1023
- cwd: spec.cwd,
1024
- command: "docker",
1025
- args: buildDockerArgs({ ...spec, volume: { ...spec.volume, hostPath } })
1026
- },
1027
- okExitCodes,
1028
- onFailure
1029
- )
1030
- );
1031
- });
1032
- const runDockerAuthExitCode = (spec) => Effect.gen(function* (_) {
1033
- const hostPath = yield* _(resolveDockerVolumeHostPath(spec.cwd, spec.volume.hostPath));
1034
- return yield* _(
1035
- runCommandExitCode({
1036
- cwd: spec.cwd,
1037
- command: "docker",
1038
- args: buildDockerArgs({ ...spec, volume: { ...spec.volume, hostPath } })
1039
- })
1040
- );
1041
- });
1042
- const normalizeAccountLabel = (value, fallback) => {
1043
- const trimmed = value?.trim() ?? "";
1044
- if (trimmed.length === 0) {
1045
- return fallback;
1252
+ const renderNonParseError = (error) => renderPrimaryError(error) ?? renderConfigError(error) ?? renderInputError(error) ?? error.message;
1253
+ const renderError = (error) => {
1254
+ if (isParseError$1(error)) {
1255
+ return formatParseError$1(error);
1046
1256
  }
1047
- const normalized = trimmed.toLowerCase().replaceAll(/[^a-z0-9]+/g, "-");
1048
- const withoutLeading = trimLeftChar(normalized, "-");
1049
- const cleaned = trimRightChar(withoutLeading, "-");
1050
- return cleaned.length > 0 ? cleaned : fallback;
1257
+ return renderNonParseError(error);
1051
1258
  };
1052
- const buildDockerAuthSpec = (input) => ({
1053
- cwd: input.cwd,
1054
- image: input.image,
1055
- volume: { hostPath: input.hostPath, containerPath: input.containerPath },
1056
- ...typeof input.entrypoint === "string" ? { entrypoint: input.entrypoint } : {},
1057
- ...input.env === void 0 ? {} : { env: input.env },
1058
- args: input.args,
1059
- interactive: input.interactive
1060
- });
1061
1259
  const splitLines = (input) => input.replaceAll("\r\n", "\n").replaceAll("\r", "\n").split("\n");
1062
1260
  const joinLines = (lines) => lines.join("\n");
1063
1261
  const normalizeEnvText = (input) => {
@@ -1149,23 +1347,23 @@ const upsertEnvKey = (input, key, value) => {
1149
1347
  return normalizeEnvText(joinLines([...cleaned, `${trimmedKey}=${value}`]));
1150
1348
  };
1151
1349
  const removeEnvKey = (input, key) => upsertEnvKey(input, key, "");
1152
- const defaultEnvContents$1 = "# docker-git env\n# KEY=value\n";
1350
+ const defaultEnvContents = "# docker-git env\n# KEY=value\n";
1153
1351
  const ensureEnvFile$1 = (fs, path, envPath) => Effect.gen(function* (_) {
1154
1352
  const exists = yield* _(fs.exists(envPath));
1155
1353
  if (exists) {
1156
1354
  return;
1157
1355
  }
1158
1356
  yield* _(fs.makeDirectory(path.dirname(envPath), { recursive: true }));
1159
- yield* _(fs.writeFileString(envPath, defaultEnvContents$1));
1357
+ yield* _(fs.writeFileString(envPath, defaultEnvContents));
1160
1358
  });
1161
1359
  const readEnvText = (fs, envPath) => Effect.gen(function* (_) {
1162
1360
  const exists = yield* _(fs.exists(envPath));
1163
1361
  if (!exists) {
1164
- return defaultEnvContents$1;
1362
+ return defaultEnvContents;
1165
1363
  }
1166
1364
  const info = yield* _(fs.stat(envPath));
1167
1365
  if (info.type !== "File") {
1168
- return defaultEnvContents$1;
1366
+ return defaultEnvContents;
1169
1367
  }
1170
1368
  return yield* _(fs.readFileString(envPath));
1171
1369
  });
@@ -3622,15 +3820,15 @@ if [[ -n "$AGENT_PROMPT" ]]; then
3622
3820
  chmod 644 "$AGENT_PROMPT_FILE"
3623
3821
  fi`
3624
3822
  ].join("\n\n");
3625
- const renderAgentPromptCommand = (mode) => mode === "claude" ? String.raw`claude --dangerously-skip-permissions -p \"\$(cat $AGENT_PROMPT_FILE)\"` : String.raw`codex --approval-mode full-auto \"\$(cat $AGENT_PROMPT_FILE)\"`;
3823
+ const renderAgentPromptCommand = (mode) => mode === "claude" ? String.raw`claude --dangerously-skip-permissions -p \"\$(cat \"$AGENT_PROMPT_FILE\")\"` : String.raw`codex exec \"\$(cat \"$AGENT_PROMPT_FILE\")\"`;
3824
+ const renderAgentAutoLaunchCommand = (config, mode) => String.raw`su - ${config.sshUser} -s /bin/bash -c "bash -lc '. /etc/profile 2>/dev/null || true; . \"$AGENT_ENV_FILE\" 2>/dev/null || true; cd \"$TARGET_DIR\" && ${renderAgentPromptCommand(mode)}'"`;
3626
3825
  const renderAgentModeBlock = (config, mode) => {
3627
3826
  const startMessage = `[agent] starting ${mode}...`;
3628
3827
  const interactiveMessage = `[agent] ${mode} started in interactive mode (use SSH to connect)`;
3629
3828
  return String.raw`"${mode}")
3630
3829
  echo "${startMessage}"
3631
3830
  if [[ -n "$AGENT_PROMPT" ]]; then
3632
- if su - ${config.sshUser} \
3633
- -c ". /run/docker-git/agent-env.sh 2>/dev/null; cd '$TARGET_DIR' && ${renderAgentPromptCommand(mode)}"; then
3831
+ if ${renderAgentAutoLaunchCommand(config, mode)}; then
3634
3832
  AGENT_OK=1
3635
3833
  fi
3636
3834
  else
@@ -3961,6 +4159,8 @@ const renderAgentModeEnv = (agentMode) => agentMode !== void 0 && agentMode.leng
3961
4159
  ` : "";
3962
4160
  const renderAgentAutoEnv = (agentAuto) => agentAuto === true ? ` AGENT_AUTO: "1"
3963
4161
  ` : "";
4162
+ const renderProjectsRootHostMount = (projectsRoot) => `\${DOCKER_GIT_PROJECTS_ROOT_HOST:-${projectsRoot}}`;
4163
+ const renderSharedCodexHostMount = (projectsRoot) => `\${DOCKER_GIT_PROJECTS_ROOT_HOST:-${projectsRoot}}/.orch/auth/codex`;
3964
4164
  const buildPlaywrightFragments = (config, networkName) => {
3965
4165
  if (!config.enableMcpPlaywright) {
3966
4166
  return {
@@ -4052,10 +4252,10 @@ ${fragments.maybePlaywrightEnv}${fragments.maybeDependsOn} env_file:
4052
4252
  - "127.0.0.1:${config.sshPort}:22"
4053
4253
  volumes:
4054
4254
  - ${config.volumeName}:/home/${config.sshUser}
4055
- - ${config.dockerGitPath}:/home/${config.sshUser}/.docker-git
4255
+ - ${renderProjectsRootHostMount(config.dockerGitPath)}:/home/${config.sshUser}/.docker-git
4056
4256
  - ${config.authorizedKeysPath}:/authorized_keys:ro
4057
4257
  - ${config.codexAuthPath}:${config.codexHome}
4058
- - ${config.codexSharedAuthPath}:${config.codexHome}-shared
4258
+ - ${renderSharedCodexHostMount(config.dockerGitPath)}:${config.codexHome}-shared
4059
4259
  - /var/run/docker.sock:/var/run/docker.sock
4060
4260
  networks:
4061
4261
  - ${fragments.networkName}
@@ -4101,7 +4301,7 @@ RUN oh-my-opencode --version
4101
4301
  RUN npm install -g @anthropic-ai/claude-code@latest
4102
4302
  RUN claude --version`;
4103
4303
  const renderDockerfileOpenCode = () => `# Tooling: OpenCode (binary)
4104
- RUN curl -fsSL https://opencode.ai/install | HOME=/usr/local bash -s -- --no-modify-path
4304
+ RUN set -eu; for attempt in 1 2 3 4 5; do if curl -fsSL --retry 5 --retry-all-errors --retry-delay 2 https://opencode.ai/install | HOME=/usr/local bash -s -- --no-modify-path; then exit 0; fi; echo "opencode install attempt \${attempt} failed; retrying..." >&2; sleep $((attempt * 2)); done; echo "opencode install failed after retries" >&2; exit 1
4105
4305
  RUN ln -sf /usr/local/.opencode/bin/opencode /usr/local/bin/opencode
4106
4306
  RUN opencode --version`;
4107
4307
  const gitleaksVersion = "8.28.0";
@@ -4991,77 +5191,6 @@ const copyDirIfEmpty = (fs, path, sourceDir, targetDir, label) => Effect.gen(fun
4991
5191
  yield* _(copyDirRecursive(fs, path, sourceDir, targetDir));
4992
5192
  yield* _(Effect.log(`Copied ${label} from ${sourceDir} to ${targetDir}`));
4993
5193
  });
4994
- const JsonValueSchema = Schema.suspend(
4995
- () => Schema.Union(
4996
- Schema.Null,
4997
- Schema.Boolean,
4998
- Schema.String,
4999
- Schema.JsonNumber,
5000
- Schema.Array(JsonValueSchema),
5001
- Schema.Record({ key: Schema.String, value: JsonValueSchema })
5002
- )
5003
- );
5004
- const JsonRecordSchema = Schema.Record({
5005
- key: Schema.String,
5006
- value: JsonValueSchema
5007
- });
5008
- const JsonRecordFromStringSchema = Schema.parseJson(JsonRecordSchema);
5009
- const defaultEnvContents = "# docker-git env\n# KEY=value\n";
5010
- const codexConfigMarker = "# docker-git codex config";
5011
- const defaultCodexConfig = [
5012
- "# docker-git codex config",
5013
- 'model = "gpt-5.4"',
5014
- "model_context_window = 1050000",
5015
- "model_auto_compact_token_limit = 945000",
5016
- 'model_reasoning_effort = "xhigh"',
5017
- 'plan_mode_reasoning_effort = "xhigh"',
5018
- 'personality = "pragmatic"',
5019
- "",
5020
- 'approval_policy = "never"',
5021
- 'sandbox_mode = "danger-full-access"',
5022
- 'web_search = "live"',
5023
- "",
5024
- "[features]",
5025
- "shell_snapshot = true",
5026
- "multi_agent = true",
5027
- "apps = true",
5028
- "shell_tool = true"
5029
- ].join("\n");
5030
- const resolvePathFromBase$1 = (path, baseDir, targetPath) => path.isAbsolute(targetPath) ? targetPath : path.resolve(baseDir, targetPath);
5031
- const isPermissionDeniedSystemError = (error) => error._tag === "SystemError" && error.reason === "PermissionDenied";
5032
- const skipCodexConfigPermissionDenied = (configPath, error) => isPermissionDeniedSystemError(error) ? Effect.logWarning(
5033
- `Skipped Codex config sync at ${configPath}: permission denied (${error.description ?? "no details"}).`
5034
- ) : Effect.fail(error);
5035
- const normalizeConfigText = (text) => text.replaceAll("\r\n", "\n").trim();
5036
- const shouldRewriteDockerGitCodexConfig = (existing) => {
5037
- const normalized = normalizeConfigText(existing);
5038
- if (normalized.length === 0) {
5039
- return true;
5040
- }
5041
- if (!normalized.startsWith(codexConfigMarker)) {
5042
- return false;
5043
- }
5044
- return normalized !== normalizeConfigText(defaultCodexConfig);
5045
- };
5046
- const shouldCopyEnv = (sourceText, targetText) => {
5047
- if (sourceText.trim().length === 0) {
5048
- return "skip";
5049
- }
5050
- if (targetText.trim().length === 0) {
5051
- return "copy";
5052
- }
5053
- if (targetText.trim() === defaultEnvContents.trim() && sourceText.trim() !== defaultEnvContents.trim()) {
5054
- return "copy";
5055
- }
5056
- return "skip";
5057
- };
5058
- const parseJsonRecord = (text) => Either.match(ParseResult.decodeUnknownEither(JsonRecordFromStringSchema)(text), {
5059
- onLeft: () => Effect.succeed(null),
5060
- onRight: (record) => Effect.succeed(record)
5061
- });
5062
- const hasClaudeOauthAccount = (record) => record !== null && typeof record["oauthAccount"] === "object" && record["oauthAccount"] !== null;
5063
- const hasClaudeCredentials = (record) => record !== null && typeof record["claudeAiOauth"] === "object" && record["claudeAiOauth"] !== null;
5064
- const isGithubTokenKey = (key) => key === "GITHUB_TOKEN" || key === "GH_TOKEN" || key.startsWith("GITHUB_TOKEN__");
5065
5194
  const syncClaudeJsonFile = (fs, path, spec) => Effect.gen(function* (_) {
5066
5195
  const sourceExists = yield* _(fs.exists(spec.sourcePath));
5067
5196
  if (!sourceExists) {
@@ -5112,18 +5241,6 @@ const syncClaudeCredentialsJson = (fs, path, sourcePath, targetPath) => syncClau
5112
5241
  seedLabel: "Claude credentials",
5113
5242
  updateLabel: "Claude credentials"
5114
5243
  });
5115
- const hasNonEmptyFile = (fs, filePath) => Effect.gen(function* (_) {
5116
- const exists = yield* _(fs.exists(filePath));
5117
- if (!exists) {
5118
- return false;
5119
- }
5120
- const info = yield* _(fs.stat(filePath));
5121
- if (info.type !== "File") {
5122
- return false;
5123
- }
5124
- const text = yield* _(fs.readFileString(filePath), Effect.orElseSucceed(() => ""));
5125
- return text.trim().length > 0;
5126
- });
5127
5244
  const ensureClaudeAuthSeedFromHome = (baseDir, claudeAuthPath) => withFsPathContext(
5128
5245
  ({ fs, path }) => Effect.gen(function* (_) {
5129
5246
  const homeDir = (process.env["HOME"] ?? "").trim();
@@ -6046,6 +6163,20 @@ const maybeOpenSsh = (command, hasAgent, waitForAgent, projectConfig) => Effect.
6046
6163
  const remoteCommand = resolveInteractiveRemoteCommand(projectConfig, interactiveAgent);
6047
6164
  yield* _(openSshBestEffort(projectConfig, remoteCommand));
6048
6165
  }).pipe(Effect.asVoid);
6166
+ const resolveFinalAgentConfig = (resolvedConfig) => Effect.gen(function* (_) {
6167
+ const resolvedAgentMode = yield* _(resolveAutoAgentMode(resolvedConfig));
6168
+ if ((resolvedConfig.agentAuto ?? false) && resolvedConfig.agentMode === void 0 && resolvedAgentMode !== void 0) {
6169
+ yield* _(Effect.log(`Auto agent selected: ${resolvedAgentMode}`));
6170
+ }
6171
+ return resolvedAgentMode === void 0 ? resolvedConfig : { ...resolvedConfig, agentMode: resolvedAgentMode };
6172
+ });
6173
+ const maybeCleanupAfterAgent = (waitForAgent, resolvedOutDir) => Effect.gen(function* (_) {
6174
+ if (!waitForAgent) {
6175
+ return;
6176
+ }
6177
+ yield* _(Effect.log("Agent finished. Cleaning up container..."));
6178
+ yield* _(runDockerDownCleanup(resolvedOutDir));
6179
+ });
6049
6180
  const runCreateProject = (path, command) => Effect.gen(function* (_) {
6050
6181
  if (command.runUp) {
6051
6182
  yield* _(ensureDockerDaemonAccess(process.cwd()));
@@ -6053,7 +6184,8 @@ const runCreateProject = (path, command) => Effect.gen(function* (_) {
6053
6184
  const ctx = makeCreateContext(path, process.cwd());
6054
6185
  const resolvedOutDir = path.resolve(ctx.resolveRootPath(command.outDir));
6055
6186
  const resolvedConfig = yield* _(resolveCreateConfig(command, ctx, resolvedOutDir));
6056
- const { globalConfig, projectConfig } = buildProjectConfigs(path, ctx.baseDir, resolvedOutDir, resolvedConfig);
6187
+ const finalConfig = yield* _(resolveFinalAgentConfig(resolvedConfig));
6188
+ const { globalConfig, projectConfig } = buildProjectConfigs(path, ctx.baseDir, resolvedOutDir, finalConfig);
6057
6189
  yield* _(migrateProjectOrchLayout(ctx.baseDir, globalConfig, ctx.resolveRootPath));
6058
6190
  const createdFiles = yield* _(
6059
6191
  prepareProjectFiles(resolvedOutDir, ctx.baseDir, globalConfig, projectConfig, {
@@ -6062,8 +6194,8 @@ const runCreateProject = (path, command) => Effect.gen(function* (_) {
6062
6194
  })
6063
6195
  );
6064
6196
  yield* _(logCreatedProject(resolvedOutDir, createdFiles));
6065
- const hasAgent = resolvedConfig.agentMode !== void 0;
6066
- const waitForAgent = hasAgent && (resolvedConfig.agentAuto ?? false);
6197
+ const hasAgent = finalConfig.agentMode !== void 0;
6198
+ const waitForAgent = hasAgent && (finalConfig.agentAuto ?? false);
6067
6199
  yield* _(
6068
6200
  runDockerUpIfNeeded(resolvedOutDir, projectConfig, {
6069
6201
  runUp: command.runUp,
@@ -6076,10 +6208,7 @@ const runCreateProject = (path, command) => Effect.gen(function* (_) {
6076
6208
  if (command.runUp) {
6077
6209
  yield* _(logDockerAccessInfo(resolvedOutDir, projectConfig));
6078
6210
  }
6079
- if (waitForAgent) {
6080
- yield* _(Effect.log("Agent finished. Cleaning up container..."));
6081
- yield* _(runDockerDownCleanup(resolvedOutDir));
6082
- }
6211
+ yield* _(maybeCleanupAfterAgent(waitForAgent, resolvedOutDir));
6083
6212
  yield* _(autoSyncState(`chore(state): update ${formatStateSyncLabel(projectConfig.repoUrl)}`));
6084
6213
  yield* _(maybeOpenSsh(command, hasAgent, waitForAgent, projectConfig));
6085
6214
  }).pipe(Effect.asVoid);
@@ -7734,7 +7863,8 @@ const valueOptionSpecs = [
7734
7863
  { flag: "-m", key: "message" },
7735
7864
  { flag: "--out-dir", key: "outDir" },
7736
7865
  { flag: "--project-dir", key: "projectDir" },
7737
- { flag: "--lines", key: "lines" }
7866
+ { flag: "--lines", key: "lines" },
7867
+ { flag: "--auto", key: "agentAutoMode" }
7738
7868
  ];
7739
7869
  const valueOptionSpecByFlag = new Map(
7740
7870
  valueOptionSpecs.map((spec) => [spec.flag, spec])
@@ -7752,9 +7882,7 @@ const booleanFlagUpdaters = {
7752
7882
  "--no-wipe": (raw) => ({ ...raw, wipe: false }),
7753
7883
  "--web": (raw) => ({ ...raw, authWeb: true }),
7754
7884
  "--include-default": (raw) => ({ ...raw, includeDefault: true }),
7755
- "--claude": (raw) => ({ ...raw, agentClaude: true }),
7756
- "--codex": (raw) => ({ ...raw, agentCodex: true }),
7757
- "--auto": (raw) => ({ ...raw, agentAuto: true })
7885
+ "--auto": (raw) => ({ ...raw, agentAutoMode: "auto" })
7758
7886
  };
7759
7887
  const valueFlagUpdaters = {
7760
7888
  repoUrl: (raw, value) => ({ ...raw, repoUrl: value }),
@@ -7784,7 +7912,8 @@ const valueFlagUpdaters = {
7784
7912
  message: (raw, value) => ({ ...raw, message: value }),
7785
7913
  outDir: (raw, value) => ({ ...raw, outDir: value }),
7786
7914
  projectDir: (raw, value) => ({ ...raw, projectDir: value }),
7787
- lines: (raw, value) => ({ ...raw, lines: value })
7915
+ lines: (raw, value) => ({ ...raw, lines: value }),
7916
+ agentAutoMode: (raw, value) => ({ ...raw, agentAutoMode: value.trim().toLowerCase() })
7788
7917
  };
7789
7918
  const applyCommandBooleanFlag = (raw, token) => {
7790
7919
  const updater = booleanFlagUpdaters[token];
@@ -7807,25 +7936,55 @@ const parseInlineValueToken = (raw, token) => {
7807
7936
  const inlineValue = token.slice(equalIndex + 1);
7808
7937
  return applyCommandValueFlag(raw, flag, inlineValue);
7809
7938
  };
7810
- const parseRawOptionsStep = (args, index, raw) => {
7811
- const token = args[index] ?? "";
7939
+ const legacyAgentFlagError = (token) => {
7940
+ if (token === "--claude") {
7941
+ return {
7942
+ _tag: "InvalidOption",
7943
+ option: token,
7944
+ reason: "use --auto=claude"
7945
+ };
7946
+ }
7947
+ if (token === "--codex") {
7948
+ return {
7949
+ _tag: "InvalidOption",
7950
+ option: token,
7951
+ reason: "use --auto=codex"
7952
+ };
7953
+ }
7954
+ return null;
7955
+ };
7956
+ const toParseStep = (parsed, nextIndex) => Either.isLeft(parsed) ? { _tag: "error", error: parsed.left } : { _tag: "ok", raw: parsed.right, nextIndex };
7957
+ const parseValueOptionStep = (raw, token, value, index) => {
7958
+ if (value === void 0) {
7959
+ return { _tag: "error", error: { _tag: "MissingOptionValue", option: token } };
7960
+ }
7961
+ return toParseStep(applyCommandValueFlag(raw, token, value), index + 2);
7962
+ };
7963
+ const parseSpecialFlagStep = (raw, token, index) => {
7812
7964
  const inlineApplied = parseInlineValueToken(raw, token);
7813
7965
  if (inlineApplied !== null) {
7814
- return Either.isLeft(inlineApplied) ? { _tag: "error", error: inlineApplied.left } : { _tag: "ok", raw: inlineApplied.right, nextIndex: index + 1 };
7966
+ return toParseStep(inlineApplied, index + 1);
7815
7967
  }
7816
7968
  const booleanApplied = applyCommandBooleanFlag(raw, token);
7817
7969
  if (booleanApplied !== null) {
7818
7970
  return { _tag: "ok", raw: booleanApplied, nextIndex: index + 1 };
7819
7971
  }
7972
+ const deprecatedAgentFlag = legacyAgentFlagError(token);
7973
+ if (deprecatedAgentFlag !== null) {
7974
+ return { _tag: "error", error: deprecatedAgentFlag };
7975
+ }
7976
+ return null;
7977
+ };
7978
+ const parseRawOptionsStep = (args, index, raw) => {
7979
+ const token = args[index] ?? "";
7980
+ const specialStep = parseSpecialFlagStep(raw, token, index);
7981
+ if (specialStep !== null) {
7982
+ return specialStep;
7983
+ }
7820
7984
  if (!token.startsWith("-")) {
7821
7985
  return { _tag: "error", error: { _tag: "UnexpectedArgument", value: token } };
7822
7986
  }
7823
- const value = args[index + 1];
7824
- if (value === void 0) {
7825
- return { _tag: "error", error: { _tag: "MissingOptionValue", option: token } };
7826
- }
7827
- const nextRaw = applyCommandValueFlag(raw, token, value);
7828
- return Either.isLeft(nextRaw) ? { _tag: "error", error: nextRaw.left } : { _tag: "ok", raw: nextRaw.right, nextIndex: index + 2 };
7987
+ return parseValueOptionStep(raw, token, args[index + 1], index);
7829
7988
  };
7830
7989
  const parseRawOptions = (args) => {
7831
7990
  let index = 0;
@@ -7975,6 +8134,23 @@ const parseAuth = (args) => {
7975
8134
  const rest = args.slice(2);
7976
8135
  return Either.flatMap(parseRawOptions(rest), (raw) => buildAuthCommand(provider, action, resolveAuthOptions(raw)));
7977
8136
  };
8137
+ const resolveAutoAgentFlags = (raw) => {
8138
+ const requested = raw.agentAutoMode;
8139
+ if (requested === void 0) {
8140
+ return Either.right({ agentMode: void 0, agentAuto: false });
8141
+ }
8142
+ if (requested === "auto") {
8143
+ return Either.right({ agentMode: void 0, agentAuto: true });
8144
+ }
8145
+ if (requested === "claude" || requested === "codex") {
8146
+ return Either.right({ agentMode: requested, agentAuto: true });
8147
+ }
8148
+ return Either.left({
8149
+ _tag: "InvalidOption",
8150
+ option: "--auto",
8151
+ reason: "expected one of: claude, codex"
8152
+ });
8153
+ };
7978
8154
  const parsePort = (value) => {
7979
8155
  const parsed = Number(value);
7980
8156
  if (!Number.isInteger(parsed)) {
@@ -8100,11 +8276,6 @@ const resolveCreateBehavior = (raw) => ({
8100
8276
  forceEnv: raw.forceEnv ?? false,
8101
8277
  enableMcpPlaywright: raw.enableMcpPlaywright ?? false
8102
8278
  });
8103
- const resolveAgentMode = (raw) => {
8104
- if (raw.agentClaude) return "claude";
8105
- if (raw.agentCodex) return "codex";
8106
- return void 0;
8107
- };
8108
8279
  const buildTemplateConfig = ({
8109
8280
  agentAuto,
8110
8281
  agentMode,
@@ -8155,8 +8326,7 @@ const buildCreateCommand = (raw) => Either.gen(function* (_) {
8155
8326
  const dockerSharedNetworkName = yield* _(
8156
8327
  nonEmpty("--shared-network", raw.dockerSharedNetworkName, defaultTemplateConfig.dockerSharedNetworkName)
8157
8328
  );
8158
- const agentMode = resolveAgentMode(raw);
8159
- const agentAuto = raw.agentAuto ?? false;
8329
+ const { agentAuto, agentMode } = yield* _(resolveAutoAgentFlags(raw));
8160
8330
  return {
8161
8331
  _tag: "Create",
8162
8332
  outDir: paths.outDir,
@@ -8480,9 +8650,7 @@ Options:
8480
8650
  --up | --no-up Run docker compose up after init (default: --up)
8481
8651
  --ssh | --no-ssh Auto-open SSH after create/clone (default: clone=--ssh, create=--no-ssh)
8482
8652
  --mcp-playwright | --no-mcp-playwright Enable Playwright MCP + Chromium sidecar (default: --no-mcp-playwright)
8483
- --claude Start Claude Code agent inside container after clone
8484
- --codex Start Codex agent inside container after clone
8485
- --auto Auto-execute: agent completes the task, creates PR and pushes (requires --claude or --codex)
8653
+ --auto[=claude|codex] Auto-execute an agent; without value picks by auth, random if both are available
8486
8654
  --force Overwrite existing files and wipe compose volumes (docker compose down -v)
8487
8655
  --force-env Reset project env defaults only (keep workspace volume/data)
8488
8656
  -h, --help Show this help