@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.
- package/dist/src/docker-git/main.js +593 -425
- package/dist/src/docker-git/main.js.map +1 -1
- package/package.json +1 -1
|
@@ -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
|
|
517
|
-
cwd
|
|
518
|
-
|
|
519
|
-
|
|
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
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
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
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
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
|
|
830
|
-
|
|
831
|
-
|
|
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 (
|
|
834
|
-
return
|
|
1040
|
+
if (!normalized.startsWith(codexConfigMarker)) {
|
|
1041
|
+
return false;
|
|
835
1042
|
}
|
|
836
|
-
return
|
|
1043
|
+
return normalized !== normalizeConfigText(defaultCodexConfig);
|
|
837
1044
|
};
|
|
838
|
-
const
|
|
839
|
-
if (
|
|
840
|
-
return "
|
|
1045
|
+
const shouldCopyEnv = (sourceText, targetText) => {
|
|
1046
|
+
if (sourceText.trim().length === 0) {
|
|
1047
|
+
return "skip";
|
|
841
1048
|
}
|
|
842
|
-
if (
|
|
843
|
-
return
|
|
1049
|
+
if (targetText.trim().length === 0) {
|
|
1050
|
+
return "copy";
|
|
844
1051
|
}
|
|
845
|
-
|
|
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
|
|
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
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
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
|
-
|
|
868
|
-
|
|
869
|
-
|
|
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
|
|
877
|
-
return
|
|
878
|
-
};
|
|
879
|
-
const
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
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
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
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
|
|
900
|
-
const
|
|
901
|
-
if (
|
|
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 [
|
|
910
|
-
}
|
|
911
|
-
const
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
if (
|
|
915
|
-
|
|
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
|
-
|
|
918
|
-
|
|
1107
|
+
const credentials = yield* _(isRegularFile$1(fs, `${accountPath}/.credentials.json`));
|
|
1108
|
+
if (credentials) {
|
|
1109
|
+
return true;
|
|
919
1110
|
}
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
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
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
const
|
|
940
|
-
|
|
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
|
-
|
|
1123
|
+
const codexAvailable = yield* _(hasCodexAuth(fs, config.codexSharedAuthPath, config.codexAuthLabel));
|
|
1124
|
+
return { claudeAvailable, codexAvailable };
|
|
956
1125
|
});
|
|
957
|
-
const
|
|
958
|
-
|
|
959
|
-
|
|
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
|
-
|
|
964
|
-
|
|
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
|
|
1133
|
+
return Effect.sync(() => mode);
|
|
969
1134
|
};
|
|
970
|
-
const
|
|
971
|
-
if (
|
|
972
|
-
|
|
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
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
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
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
if (
|
|
990
|
-
|
|
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 (
|
|
993
|
-
|
|
1238
|
+
if (error._tag === "ConfigDecodeError") {
|
|
1239
|
+
return `Invalid docker-git.json at ${error.path}: ${error.message}`;
|
|
994
1240
|
}
|
|
995
|
-
|
|
996
|
-
|
|
1241
|
+
return null;
|
|
1242
|
+
};
|
|
1243
|
+
const renderInputError = (error) => {
|
|
1244
|
+
if (error._tag === "InputCancelledError") {
|
|
1245
|
+
return "Input cancelled.";
|
|
997
1246
|
}
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
appendEnvArgs(base, spec.env);
|
|
1247
|
+
if (error._tag === "InputReadError") {
|
|
1248
|
+
return `Input error: ${error.message}`;
|
|
1001
1249
|
}
|
|
1002
|
-
return
|
|
1250
|
+
return null;
|
|
1003
1251
|
};
|
|
1004
|
-
const
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
1362
|
+
return defaultEnvContents;
|
|
1165
1363
|
}
|
|
1166
1364
|
const info = yield* _(fs.stat(envPath));
|
|
1167
1365
|
if (info.type !== "File") {
|
|
1168
|
-
return defaultEnvContents
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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 =
|
|
6066
|
-
const waitForAgent = hasAgent && (
|
|
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
|
-
|
|
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
|
-
"--
|
|
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
|
|
7811
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|