@prover-coder-ai/docker-git 1.0.41 → 1.0.43

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.
@@ -269,6 +269,8 @@ var defaultTemplateConfig = {
269
269
  codexAuthPath: "./.docker-git/.orch/auth/codex",
270
270
  codexSharedAuthPath: "./.docker-git/.orch/auth/codex",
271
271
  codexHome: "/home/dev/.codex",
272
+ cpuLimit: "30%",
273
+ ramLimit: "30%",
272
274
  dockerNetworkMode: defaultDockerNetworkMode,
273
275
  dockerSharedNetworkName: defaultDockerSharedNetworkName,
274
276
  enableMcpPlaywright: false,
@@ -1209,6 +1211,38 @@ var ensureGhAuthImage = (fs, path, cwd, buildLabel) => ensureDockerImage(fs, pat
1209
1211
  buildLabel
1210
1212
  });
1211
1213
  //#endregion
1214
+ //#region ../lib/src/usecases/github-api-helpers.ts
1215
+ /**
1216
+ * Run `gh api <args>` inside the auth Docker container and return trimmed stdout.
1217
+ *
1218
+ * @pure false
1219
+ * @effect CommandExecutor (Docker)
1220
+ * @invariant exits with CommandFailedError on non-zero exit code
1221
+ * @complexity O(1)
1222
+ */
1223
+ var runGhApiCapture = (cwd, hostPath, token, args) => runDockerAuthCapture(buildDockerAuthSpec({
1224
+ cwd,
1225
+ image: ghImageName,
1226
+ hostPath,
1227
+ containerPath: ghAuthDir,
1228
+ env: `GH_TOKEN=${token}`,
1229
+ args: ["api", ...args],
1230
+ interactive: false
1231
+ }), [0], (exitCode) => new CommandFailedError({
1232
+ command: `gh api ${args.join(" ")}`,
1233
+ exitCode
1234
+ })).pipe(Effect.map((raw) => raw.trim()));
1235
+ /**
1236
+ * Like `runGhApiCapture` but returns `null` instead of failing on API errors
1237
+ * (e.g. HTTP 404 / non-zero exit code).
1238
+ *
1239
+ * @pure false
1240
+ * @effect CommandExecutor (Docker)
1241
+ * @invariant never fails — errors become null
1242
+ * @complexity O(1)
1243
+ */
1244
+ var runGhApiNullable = (cwd, hostPath, token, args) => runGhApiCapture(cwd, hostPath, token, args).pipe(Effect.catchTag("CommandFailedError", () => Effect.succeed("")), Effect.map((raw) => raw.length === 0 ? null : raw));
1245
+ //#endregion
1212
1246
  //#region ../lib/src/usecases/runtime.ts
1213
1247
  var withFsPathContext = (run) => Effect.gen(function* (_) {
1214
1248
  return yield* _(run({
@@ -1226,20 +1260,7 @@ var resolveGithubToken$1 = (envText) => {
1226
1260
  const labeled = entries.find((entry) => entry.key.startsWith("GITHUB_TOKEN__"));
1227
1261
  return labeled && labeled.value.trim().length > 0 ? labeled.value.trim() : null;
1228
1262
  };
1229
- var runGhApiCapture = (cwd, hostPath, token, args) => runDockerAuthCapture(buildDockerAuthSpec({
1230
- cwd,
1231
- image: ghImageName,
1232
- hostPath,
1233
- containerPath: ghAuthDir,
1234
- env: `GH_TOKEN=${token}`,
1235
- args: ["api", ...args],
1236
- interactive: false
1237
- }), [0], (exitCode) => new CommandFailedError({
1238
- command: `gh api ${args.join(" ")}`,
1239
- exitCode
1240
- })).pipe(Effect.map((raw) => raw.trim()));
1241
- var runGhApiCloneUrl = (cwd, hostPath, token, args) => runGhApiCapture(cwd, hostPath, token, args).pipe(Effect.catchTag("CommandFailedError", () => Effect.succeed("")), Effect.map((raw) => raw.length === 0 ? null : raw));
1242
- var resolveViewerLogin = (cwd, hostPath, token) => Effect.gen(function* (_) {
1263
+ var resolveViewerLogin$1 = (cwd, hostPath, token) => Effect.gen(function* (_) {
1243
1264
  const command = "gh api /user --jq .login";
1244
1265
  const raw = yield* _(runGhApiCapture(cwd, hostPath, token, [
1245
1266
  "/user",
@@ -1252,12 +1273,12 @@ var resolveViewerLogin = (cwd, hostPath, token) => Effect.gen(function* (_) {
1252
1273
  })));
1253
1274
  return raw;
1254
1275
  });
1255
- var resolveRepoCloneUrl = (cwd, hostPath, token, fullName) => runGhApiCloneUrl(cwd, hostPath, token, [
1276
+ var resolveRepoCloneUrl = (cwd, hostPath, token, fullName) => runGhApiNullable(cwd, hostPath, token, [
1256
1277
  `/repos/${fullName}`,
1257
1278
  "--jq",
1258
1279
  ".clone_url"
1259
1280
  ]);
1260
- var createFork = (cwd, hostPath, token, owner, repo) => runGhApiCloneUrl(cwd, hostPath, token, [
1281
+ var createFork = (cwd, hostPath, token, owner, repo) => runGhApiNullable(cwd, hostPath, token, [
1261
1282
  "-X",
1262
1283
  "POST",
1263
1284
  `/repos/${owner}/${repo}/forks`,
@@ -1275,7 +1296,7 @@ var resolveGithubForkUrl = (repoUrl, envGlobalPath) => withFsPathContext(({ cwd,
1275
1296
  const ghRoot = resolvePathFromCwd(path, cwd, ghAuthRoot);
1276
1297
  yield* _(fs.makeDirectory(ghRoot, { recursive: true }));
1277
1298
  yield* _(ensureGhAuthImage(fs, path, cwd, "gh api"));
1278
- const viewer = yield* _(resolveViewerLogin(cwd, ghRoot, token));
1299
+ const viewer = yield* _(resolveViewerLogin$1(cwd, ghRoot, token));
1279
1300
  if (viewer.toLowerCase() === repo.owner.toLowerCase()) return null;
1280
1301
  const existingFork = yield* _(resolveRepoCloneUrl(cwd, ghRoot, token, `${viewer}/${repo.repo}`));
1281
1302
  if (existingFork !== null) return existingFork;
@@ -1336,6 +1357,8 @@ var TemplateConfigSchema = Schema.Struct({
1336
1357
  codexAuthPath: Schema.String,
1337
1358
  codexSharedAuthPath: Schema.optionalWith(Schema.String, { default: () => defaultTemplateConfig.codexSharedAuthPath }),
1338
1359
  codexHome: Schema.String,
1360
+ cpuLimit: Schema.optionalWith(Schema.String, { default: () => defaultTemplateConfig.cpuLimit }),
1361
+ ramLimit: Schema.optionalWith(Schema.String, { default: () => defaultTemplateConfig.ramLimit }),
1339
1362
  dockerNetworkMode: Schema.optionalWith(Schema.Literal("shared", "project"), { default: () => defaultTemplateConfig.dockerNetworkMode }),
1340
1363
  dockerSharedNetworkName: Schema.optionalWith(Schema.String, { default: () => defaultTemplateConfig.dockerSharedNetworkName }),
1341
1364
  enableMcpPlaywright: Schema.optionalWith(Schema.Boolean, { default: () => defaultTemplateConfig.enableMcpPlaywright }),
@@ -1506,28 +1529,93 @@ var withProjectIndexAndSsh = (run) => pipe(loadProjectIndex(), Effect.flatMap((i
1506
1529
  return yield* _(run(index, yield* _(findSshPrivateKey(yield* _(FileSystem.FileSystem), yield* _(Path.Path), process.cwd()))));
1507
1530
  })));
1508
1531
  //#endregion
1509
- //#region ../lib/src/usecases/state-repo/env.ts
1510
- var isTruthyEnv = (value) => {
1511
- const normalized = value.trim().toLowerCase();
1512
- return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
1532
+ //#region ../lib/src/core/resource-limits.ts
1533
+ var mebibyte = 1024 ** 2;
1534
+ var minimumResolvedCpuLimit = .25;
1535
+ var minimumResolvedRamLimitMib = 512;
1536
+ var precisionScale = 100;
1537
+ var cpuAbsolutePattern = /^\d+(?:\.\d+)?$/u;
1538
+ var ramAbsolutePattern = /^\d+(?:\.\d+)?(?:b|k|kb|m|mb|g|gb|t|tb)$/iu;
1539
+ var percentPattern = /^\d+(?:\.\d+)?%$/u;
1540
+ var normalizePrecision = (value) => Math.round(value * precisionScale) / precisionScale;
1541
+ var missingLimit = () => void 0;
1542
+ var parsePercent = (candidate) => {
1543
+ if (!percentPattern.test(candidate)) return null;
1544
+ const parsed = Number(candidate.slice(0, -1));
1545
+ if (!Number.isFinite(parsed) || parsed <= 0 || parsed > 100) return null;
1546
+ return normalizePrecision(parsed);
1547
+ };
1548
+ var percentReason = (kind) => kind === "cpu" ? "expected CPU like 30% or 1.5" : "expected RAM like 30%, 512m or 4g";
1549
+ var normalizePercent = (candidate, kind) => {
1550
+ const parsed = parsePercent(candidate);
1551
+ if (parsed === null) return Either.left({
1552
+ _tag: "InvalidOption",
1553
+ option: kind === "cpu" ? "--cpu" : "--ram",
1554
+ reason: percentReason(kind)
1555
+ });
1556
+ return Either.right(`${parsed}%`);
1513
1557
  };
1514
- var isFalsyEnv = (value) => {
1515
- const normalized = value.trim().toLowerCase();
1516
- return normalized === "0" || normalized === "false" || normalized === "no" || normalized === "off";
1558
+ var normalizeCpuLimit = (value, option) => {
1559
+ const candidate = value?.trim().toLowerCase() ?? "";
1560
+ if (candidate.length === 0) return Either.right(missingLimit());
1561
+ if (candidate.endsWith("%")) return normalizePercent(candidate, "cpu");
1562
+ if (!cpuAbsolutePattern.test(candidate)) return Either.left({
1563
+ _tag: "InvalidOption",
1564
+ option,
1565
+ reason: "expected CPU like 30% or 1.5"
1566
+ });
1567
+ const parsed = Number(candidate);
1568
+ if (!Number.isFinite(parsed) || parsed <= 0) return Either.left({
1569
+ _tag: "InvalidOption",
1570
+ option,
1571
+ reason: "must be greater than 0"
1572
+ });
1573
+ return Either.right(String(normalizePrecision(parsed)));
1517
1574
  };
1518
- var autoSyncStrictEnvKey = "DOCKER_GIT_STATE_AUTO_SYNC_STRICT";
1519
- var defaultSyncMessage = "chore(state): sync";
1520
- var isAutoSyncEnabled = (envValue, hasRemote) => {
1521
- if (envValue === void 0) return hasRemote;
1522
- if (envValue.trim().length === 0) return hasRemote;
1523
- if (isFalsyEnv(envValue)) return false;
1524
- if (isTruthyEnv(envValue)) return true;
1525
- return true;
1575
+ var normalizeRamLimit = (value, option) => {
1576
+ const candidate = value?.trim().toLowerCase() ?? "";
1577
+ if (candidate.length === 0) return Either.right(missingLimit());
1578
+ if (candidate.endsWith("%")) return normalizePercent(candidate, "ram");
1579
+ if (!ramAbsolutePattern.test(candidate)) return Either.left({
1580
+ _tag: "InvalidOption",
1581
+ option,
1582
+ reason: "expected RAM like 30%, 512m or 4g"
1583
+ });
1584
+ return Either.right(candidate);
1585
+ };
1586
+ var withDefaultResourceLimitIntent = (template) => ({
1587
+ ...template,
1588
+ cpuLimit: template.cpuLimit ?? "30%",
1589
+ ramLimit: template.ramLimit ?? "30%"
1590
+ });
1591
+ var resolvePercentCpuLimit = (percent, cpuCount) => Math.max(minimumResolvedCpuLimit, normalizePrecision(Math.max(1, cpuCount) * percent / 100));
1592
+ var resolvePercentRamLimit = (percent, totalMemoryBytes) => {
1593
+ const totalMib = Math.max(minimumResolvedRamLimitMib, Math.floor(totalMemoryBytes / mebibyte));
1594
+ return `${Math.max(minimumResolvedRamLimitMib, Math.floor(totalMib * percent / 100))}m`;
1595
+ };
1596
+ var resolveComposeResourceLimits = (template, hostResources) => {
1597
+ const cpuLimitIntent = template.cpuLimit ?? "30%";
1598
+ const ramLimitIntent = template.ramLimit ?? "30%";
1599
+ const cpuPercent = parsePercent(cpuLimitIntent);
1600
+ const ramPercent = parsePercent(ramLimitIntent);
1601
+ return {
1602
+ cpuLimit: cpuPercent === null ? Number(cpuLimitIntent) : resolvePercentCpuLimit(cpuPercent, hostResources.cpuCount),
1603
+ ramLimit: ramPercent === null ? ramLimitIntent : resolvePercentRamLimit(ramPercent, hostResources.totalMemoryBytes)
1604
+ };
1526
1605
  };
1527
1606
  //#endregion
1607
+ //#region ../lib/src/usecases/resource-limits.ts
1608
+ var resolveTemplateResourceLimits = (template) => Effect.succeed(withDefaultResourceLimitIntent(template));
1609
+ //#endregion
1528
1610
  //#region ../lib/src/usecases/state-repo/git-commands.ts
1529
1611
  var successExitCode = Number(ExitCode(0));
1530
- var gitBaseEnv$1 = { GIT_TERMINAL_PROMPT: "0" };
1612
+ var gitBaseEnv$1 = {
1613
+ GIT_TERMINAL_PROMPT: "0",
1614
+ GIT_AUTHOR_NAME: "docker-git",
1615
+ GIT_AUTHOR_EMAIL: "docker-git@users.noreply.github.com",
1616
+ GIT_COMMITTER_NAME: "docker-git",
1617
+ GIT_COMMITTER_EMAIL: "docker-git@users.noreply.github.com"
1618
+ };
1531
1619
  var git = (cwd, args, env = gitBaseEnv$1) => runCommandWithExitCodes({
1532
1620
  cwd,
1533
1621
  command: "git",
@@ -1543,7 +1631,7 @@ var gitExitCode = (cwd, args, env = gitBaseEnv$1) => runCommandExitCode({
1543
1631
  args,
1544
1632
  env
1545
1633
  });
1546
- var gitCapture = (cwd, args, env = gitBaseEnv$1) => runCommandCapture({
1634
+ var gitCapture$1 = (cwd, args, env = gitBaseEnv$1) => runCommandCapture({
1547
1635
  cwd,
1548
1636
  command: "git",
1549
1637
  args,
@@ -1559,6 +1647,68 @@ var hasOriginRemote = (root) => Effect.map(gitExitCode(root, [
1559
1647
  "origin"
1560
1648
  ]), (exit) => exit === successExitCode);
1561
1649
  //#endregion
1650
+ //#region ../lib/src/usecases/state-repo/adopt-remote.ts
1651
+ var adoptRemoteHistoryIfOrphan = (root, repoRef, env) => Effect.gen(function* (_) {
1652
+ const fetchExit = yield* _(gitExitCode(root, [
1653
+ "fetch",
1654
+ "origin",
1655
+ repoRef
1656
+ ], env));
1657
+ if (fetchExit !== successExitCode) {
1658
+ yield* _(Effect.logWarning(`git fetch origin ${repoRef} failed (exit ${fetchExit}); starting fresh history`));
1659
+ return;
1660
+ }
1661
+ const remoteRef = `origin/${repoRef}`;
1662
+ if ((yield* _(gitExitCode(root, [
1663
+ "show-ref",
1664
+ "--verify",
1665
+ "--quiet",
1666
+ `refs/remotes/${remoteRef}`
1667
+ ], env))) !== successExitCode) return;
1668
+ if ((yield* _(gitExitCode(root, ["rev-parse", "HEAD"], env))) !== successExitCode) {
1669
+ yield* _(git(root, ["reset", remoteRef], env));
1670
+ yield* _(gitExitCode(root, ["checkout-index", "--all"], env));
1671
+ yield* _(Effect.log(`Adopted remote history from ${remoteRef}`));
1672
+ return;
1673
+ }
1674
+ if ((yield* _(gitExitCode(root, [
1675
+ "merge-base",
1676
+ "HEAD",
1677
+ remoteRef
1678
+ ], env))) === successExitCode) return;
1679
+ yield* _(Effect.logWarning(`Local history has no common ancestor with ${remoteRef}; merging unrelated histories`));
1680
+ if ((yield* _(gitExitCode(root, [
1681
+ "merge",
1682
+ "--allow-unrelated-histories",
1683
+ "--no-edit",
1684
+ remoteRef
1685
+ ], env))) === successExitCode) {
1686
+ yield* _(Effect.log(`Merged unrelated histories from ${remoteRef}`));
1687
+ return;
1688
+ }
1689
+ yield* _(gitExitCode(root, ["merge", "--abort"], env));
1690
+ yield* _(Effect.logWarning(`Merge conflict with ${remoteRef}; sync will open a PR for manual resolution`));
1691
+ });
1692
+ //#endregion
1693
+ //#region ../lib/src/usecases/state-repo/env.ts
1694
+ var isTruthyEnv = (value) => {
1695
+ const normalized = value.trim().toLowerCase();
1696
+ return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
1697
+ };
1698
+ var isFalsyEnv = (value) => {
1699
+ const normalized = value.trim().toLowerCase();
1700
+ return normalized === "0" || normalized === "false" || normalized === "no" || normalized === "off";
1701
+ };
1702
+ var autoSyncStrictEnvKey = "DOCKER_GIT_STATE_AUTO_SYNC_STRICT";
1703
+ var defaultSyncMessage = "chore(state): sync";
1704
+ var isAutoSyncEnabled = (envValue, hasRemote) => {
1705
+ if (envValue === void 0) return hasRemote;
1706
+ if (envValue.trim().length === 0) return hasRemote;
1707
+ if (isFalsyEnv(envValue)) return false;
1708
+ if (isTruthyEnv(envValue)) return true;
1709
+ return true;
1710
+ };
1711
+ //#endregion
1562
1712
  //#region ../lib/src/usecases/state-repo/github-auth.ts
1563
1713
  var githubTokenKey = "GITHUB_TOKEN";
1564
1714
  var githubHttpsRemoteRe = /^https:\/\/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?$/;
@@ -3736,7 +3886,8 @@ var renderAgentModeEnv = (agentMode) => agentMode !== void 0 && agentMode.length
3736
3886
  var renderAgentAutoEnv = (agentAuto) => agentAuto === true ? ` AGENT_AUTO: "1"\n` : "";
3737
3887
  var renderProjectsRootHostMount = (projectsRoot) => `\${DOCKER_GIT_PROJECTS_ROOT_HOST:-${projectsRoot}}`;
3738
3888
  var renderSharedCodexHostMount = (projectsRoot) => `\${DOCKER_GIT_PROJECTS_ROOT_HOST:-${projectsRoot}}/.orch/auth/codex`;
3739
- var buildPlaywrightFragments = (config, networkName) => {
3889
+ var renderResourceLimits = (resourceLimits) => resourceLimits === void 0 ? "" : ` cpus: ${resourceLimits.cpuLimit}\n mem_limit: "${resourceLimits.ramLimit}"\n memswap_limit: "${resourceLimits.ramLimit}"\n`;
3890
+ var buildPlaywrightFragments = (config, networkName, resourceLimits) => {
3740
3891
  if (!config.enableMcpPlaywright) return {
3741
3892
  maybeDependsOn: "",
3742
3893
  maybePlaywrightEnv: "",
@@ -3751,11 +3902,11 @@ var buildPlaywrightFragments = (config, networkName) => {
3751
3902
  return {
3752
3903
  maybeDependsOn: ` depends_on:\n - ${browserServiceName}\n`,
3753
3904
  maybePlaywrightEnv: ` MCP_PLAYWRIGHT_ENABLE: "1"\n MCP_PLAYWRIGHT_CDP_ENDPOINT: "${browserCdpEndpoint}"\n`,
3754
- maybeBrowserService: `\n ${browserServiceName}:\n build:\n context: .\n dockerfile: ${browserDockerfile}\n container_name: ${browserContainerName}\n restart: unless-stopped\n environment:\n VNC_NOPW: "1"\n shm_size: "2gb"\n expose:\n - "9223"\n volumes:\n - ${browserVolumeName}:/data\n networks:\n - ${networkName}\n`,
3905
+ maybeBrowserService: `\n ${browserServiceName}:\n build:\n context: .\n dockerfile: ${browserDockerfile}\n container_name: ${browserContainerName}\n restart: unless-stopped\n${renderResourceLimits(resourceLimits)} environment:\n VNC_NOPW: "1"\n shm_size: "2gb"\n expose:\n - "9223"\n volumes:\n - ${browserVolumeName}:/data\n networks:\n - ${networkName}\n`,
3755
3906
  maybeBrowserVolume: ` ${browserVolumeName}:\n`
3756
3907
  };
3757
3908
  };
3758
- var buildComposeFragments = (config) => {
3909
+ var buildComposeFragments = (config, resourceLimits) => {
3759
3910
  const networkMode = config.dockerNetworkMode;
3760
3911
  const networkName = resolveComposeNetworkName(config);
3761
3912
  const forkRepoUrl = config.forkRepoUrl ?? "";
@@ -3767,7 +3918,7 @@ var buildComposeFragments = (config) => {
3767
3918
  const maybeClaudeAuthLabelEnv = renderClaudeAuthLabelEnv(claudeAuthLabel);
3768
3919
  const maybeAgentModeEnv = renderAgentModeEnv(config.agentMode);
3769
3920
  const maybeAgentAutoEnv = renderAgentAutoEnv(config.agentAuto);
3770
- const playwright = buildPlaywrightFragments(config, networkName);
3921
+ const playwright = buildPlaywrightFragments(config, networkName, resourceLimits);
3771
3922
  return {
3772
3923
  networkMode,
3773
3924
  networkName,
@@ -3783,7 +3934,7 @@ var buildComposeFragments = (config) => {
3783
3934
  forkRepoUrl
3784
3935
  };
3785
3936
  };
3786
- var renderComposeServices = (config, fragments) => `services:
3937
+ var renderComposeServices = (config, fragments, resourceLimits) => `services:
3787
3938
  ${config.serviceName}:
3788
3939
  build: .
3789
3940
  container_name: ${config.containerName}
@@ -3802,7 +3953,7 @@ ${fragments.maybePlaywrightEnv}${fragments.maybeDependsOn} env_file:
3802
3953
  - ${config.envProjectPath}
3803
3954
  ports:
3804
3955
  - "127.0.0.1:${config.sshPort}:22"
3805
- volumes:
3956
+ ${renderResourceLimits(resourceLimits)} volumes:
3806
3957
  - ${config.volumeName}:/home/${config.sshUser}
3807
3958
  - ${renderProjectsRootHostMount(config.dockerGitPath)}:/home/${config.sshUser}/.docker-git
3808
3959
  - ${config.authorizedKeysPath}:/authorized_keys:ro
@@ -3820,10 +3971,10 @@ var renderComposeNetworks = (networkMode, networkName) => networkMode === "share
3820
3971
  var renderComposeVolumes = (config, maybeBrowserVolume) => `volumes:
3821
3972
  ${config.volumeName}:
3822
3973
  ${maybeBrowserVolume}`;
3823
- var renderDockerCompose = (config) => {
3824
- const fragments = buildComposeFragments(config);
3974
+ var renderDockerCompose = (config, resourceLimits) => {
3975
+ const fragments = buildComposeFragments(config, resourceLimits);
3825
3976
  return [
3826
- renderComposeServices(config, fragments),
3977
+ renderComposeServices(config, fragments, resourceLimits),
3827
3978
  renderComposeNetworks(fragments.networkMode, fragments.networkName),
3828
3979
  renderComposeVolumes(config, fragments.maybeBrowserVolume)
3829
3980
  ].join("\n\n");
@@ -4097,7 +4248,7 @@ var renderConfigJson = (config) => `${JSON.stringify({
4097
4248
  template: config
4098
4249
  }, null, 2)}
4099
4250
  `;
4100
- var planFiles = (config) => {
4251
+ var planFiles = (config, composeResourceLimits) => {
4101
4252
  const maybePlaywrightFiles = config.enableMcpPlaywright ? [{
4102
4253
  _tag: "File",
4103
4254
  relativePath: "Dockerfile.browser",
@@ -4123,7 +4274,7 @@ var planFiles = (config) => {
4123
4274
  {
4124
4275
  _tag: "File",
4125
4276
  relativePath: "docker-compose.yml",
4126
- contents: renderDockerCompose(config)
4277
+ contents: renderDockerCompose(config, composeResourceLimits)
4127
4278
  },
4128
4279
  {
4129
4280
  _tag: "File",
@@ -4154,6 +4305,20 @@ var planFiles = (config) => {
4154
4305
  //#endregion
4155
4306
  //#region ../lib/src/shell/files.ts
4156
4307
  var ensureParentDir = (path, fs, filePath) => fs.makeDirectory(path.dirname(filePath), { recursive: true });
4308
+ var fallbackHostResources = {
4309
+ cpuCount: 1,
4310
+ totalMemoryBytes: 1024 ** 3
4311
+ };
4312
+ var loadHostResources = () => Effect.tryPromise({
4313
+ try: () => import("node:os").then((os) => ({
4314
+ cpuCount: os.availableParallelism(),
4315
+ totalMemoryBytes: os.totalmem()
4316
+ })),
4317
+ catch: (error) => new Error(String(error))
4318
+ }).pipe(Effect.match({
4319
+ onFailure: () => fallbackHostResources,
4320
+ onSuccess: (value) => value
4321
+ }));
4157
4322
  var isFileSpec = (spec) => spec._tag === "File";
4158
4323
  var resolveSpecPath = (path, baseDir, spec) => path.join(baseDir, spec.relativePath);
4159
4324
  var writeSpec = (path, fs, baseDir, spec) => {
@@ -4181,7 +4346,8 @@ var failOnExistingFiles = (existingFilePaths, skipExistingFiles) => {
4181
4346
  var writeProjectFiles = (outDir, config, force, skipExistingFiles = false) => Effect.gen(function* (_) {
4182
4347
  const { fs, path, resolved: baseDir } = yield* _(resolveBaseDir(outDir));
4183
4348
  yield* _(fs.makeDirectory(baseDir, { recursive: true }));
4184
- const specs = planFiles(config);
4349
+ const normalizedConfig = withDefaultResourceLimitIntent(config);
4350
+ const specs = planFiles(normalizedConfig, resolveComposeResourceLimits(normalizedConfig, yield* _(loadHostResources())));
4185
4351
  const created = [];
4186
4352
  const existingFilePaths = force ? [] : yield* _(collectExistingFilePaths(fs, path, baseDir, specs));
4187
4353
  const existingSet = new Set(existingFilePaths);
@@ -4280,7 +4446,7 @@ var commitAllIfNeeded = (root, message, env) => Effect.gen(function* (_) {
4280
4446
  ], env));
4281
4447
  });
4282
4448
  var sanitizeBranchComponent = (value) => value.trim().replaceAll(" ", "-").replaceAll(":", "-").replaceAll("..", "-").replaceAll("@{", "-").replaceAll("\\", "-").replaceAll("^", "-").replaceAll("~", "-");
4283
- var rebaseOntoOriginIfPossible = (root, baseBranch, env) => Effect.gen(function* (_) {
4449
+ var pullRemoteAndRestoreLocal = (root, baseBranch, env) => Effect.gen(function* (_) {
4284
4450
  const fetchExit = yield* _(gitExitCode(root, [
4285
4451
  "fetch",
4286
4452
  "origin",
@@ -4295,13 +4461,29 @@ var rebaseOntoOriginIfPossible = (root, baseBranch, env) => Effect.gen(function*
4295
4461
  "--verify",
4296
4462
  "--quiet",
4297
4463
  `refs/remotes/origin/${baseBranch}`
4298
- ], env))) !== successExitCode) return "skipped";
4299
- if ((yield* _(gitExitCode(root, ["rebase", `origin/${baseBranch}`], env))) === successExitCode) return "ok";
4300
- yield* _(gitExitCode(root, ["rebase", "--abort"], env));
4301
- return "conflict";
4464
+ ], env))) !== successExitCode) return;
4465
+ yield* _(git(root, ["add", "-A"], env));
4466
+ const stashExit = yield* _(gitExitCode(root, ["stash", "--include-untracked"], env));
4467
+ yield* _(git(root, [
4468
+ "reset",
4469
+ "--hard",
4470
+ `origin/${baseBranch}`
4471
+ ], env));
4472
+ if (stashExit === successExitCode) {
4473
+ if ((yield* _(gitExitCode(root, ["stash", "pop"], env))) !== successExitCode) {
4474
+ yield* _(gitExitCode(root, [
4475
+ "checkout",
4476
+ "--theirs",
4477
+ "--",
4478
+ "."
4479
+ ], env));
4480
+ yield* _(git(root, ["add", "-A"], env));
4481
+ yield* _(gitExitCode(root, ["stash", "drop"], env));
4482
+ }
4483
+ }
4302
4484
  });
4303
4485
  var pushToNewBranch = (root, baseBranch, originPushTarget, env) => Effect.gen(function* (_) {
4304
- const headShort = yield* _(gitCapture(root, [
4486
+ const headShort = yield* _(gitCapture$1(root, [
4305
4487
  "rev-parse",
4306
4488
  "--short",
4307
4489
  "HEAD"
@@ -4316,7 +4498,7 @@ var pushToNewBranch = (root, baseBranch, originPushTarget, env) => Effect.gen(fu
4316
4498
  return branch;
4317
4499
  });
4318
4500
  var resolveBaseBranch = (value) => value === "HEAD" ? "main" : value;
4319
- var getCurrentBranch = (root, env) => gitCapture(root, [
4501
+ var getCurrentBranch = (root, env) => gitCapture$1(root, [
4320
4502
  "rev-parse",
4321
4503
  "--abbrev-ref",
4322
4504
  "HEAD"
@@ -4324,15 +4506,9 @@ var getCurrentBranch = (root, env) => gitCapture(root, [
4324
4506
  var runStateSyncOps = (root, originUrl, message, env, options) => Effect.gen(function* (_) {
4325
4507
  const originPushTarget = resolveOriginPushTarget(options?.originPushUrlOverride ?? null);
4326
4508
  yield* _(normalizeLegacyStateProjects(root));
4327
- yield* _(commitAllIfNeeded(root, resolveSyncMessage(message), env));
4328
4509
  const baseBranch = resolveBaseBranch(yield* _(getCurrentBranch(root, env)));
4329
- if ((yield* _(rebaseOntoOriginIfPossible(root, baseBranch, env))) === "conflict") {
4330
- const prBranch = yield* _(pushToNewBranch(root, baseBranch, originPushTarget, env));
4331
- const compareUrl = tryBuildGithubCompareUrl(originUrl, baseBranch, prBranch);
4332
- yield* _(Effect.logWarning(`State sync needs manual merge: pushed changes to branch '${prBranch}'.`));
4333
- yield* _(logOpenPr(originUrl, baseBranch, prBranch, compareUrl));
4334
- return;
4335
- }
4510
+ yield* _(pullRemoteAndRestoreLocal(root, baseBranch, env));
4511
+ yield* _(commitAllIfNeeded(root, resolveSyncMessage(message), env));
4336
4512
  const pushExit = yield* _(gitExitCode(root, [
4337
4513
  "push",
4338
4514
  "--no-verify",
@@ -4391,7 +4567,7 @@ var stateSync = (message) => Effect.gen(function* (_) {
4391
4567
  exitCode: originUrlExit
4392
4568
  })));
4393
4569
  }
4394
- const originUrl = yield* _(gitCapture(root, [
4570
+ const originUrl = yield* _(gitCapture$1(root, [
4395
4571
  "remote",
4396
4572
  "get-url",
4397
4573
  "origin"
@@ -4419,7 +4595,7 @@ var autoSyncState = (message) => Effect.gen(function* (_) {
4419
4595
  onFailure: (error) => Effect.logWarning(`State auto-sync failed: ${String(error)}`),
4420
4596
  onSuccess: () => Effect.void
4421
4597
  }), Effect.asVoid);
4422
- var cloneStateRepo = (root, input) => Effect.gen(function* (_) {
4598
+ var cloneStateRepo = (root, input, env) => Effect.gen(function* (_) {
4423
4599
  const cloneBranchExit = yield* _(runCommandExitCode({
4424
4600
  cwd: root,
4425
4601
  command: "git",
@@ -4430,7 +4606,7 @@ var cloneStateRepo = (root, input) => Effect.gen(function* (_) {
4430
4606
  input.repoUrl,
4431
4607
  root
4432
4608
  ],
4433
- env: gitBaseEnv$1
4609
+ env
4434
4610
  }));
4435
4611
  if (cloneBranchExit === successExitCode) return;
4436
4612
  yield* _(Effect.logWarning(`git clone --branch ${input.repoRef} failed (exit ${cloneBranchExit}); retrying without --branch`));
@@ -4442,60 +4618,65 @@ var cloneStateRepo = (root, input) => Effect.gen(function* (_) {
4442
4618
  input.repoUrl,
4443
4619
  root
4444
4620
  ],
4445
- env: gitBaseEnv$1
4621
+ env
4446
4622
  }));
4447
4623
  if (cloneDefaultExit !== successExitCode) return yield* _(Effect.fail(new CommandFailedError({
4448
4624
  command: "git clone",
4449
4625
  exitCode: cloneDefaultExit
4450
4626
  })));
4451
4627
  }).pipe(Effect.asVoid);
4452
- var initRepoIfNeeded = (fs, path, root, input) => Effect.gen(function* (_) {
4628
+ var initRepoIfNeeded = (fs, path, root, input, env) => Effect.gen(function* (_) {
4453
4629
  yield* _(fs.makeDirectory(root, { recursive: true }));
4454
4630
  const gitDir = path.join(root, ".git");
4455
4631
  if (yield* _(fs.exists(gitDir))) return;
4456
4632
  if ((yield* _(fs.readDirectory(root))).length === 0) {
4457
- yield* _(cloneStateRepo(root, input));
4633
+ yield* _(cloneStateRepo(root, input, env));
4458
4634
  yield* _(Effect.log(`State dir cloned: ${root}`));
4459
4635
  return;
4460
4636
  }
4461
- yield* _(git(root, ["init"], gitBaseEnv$1));
4637
+ yield* _(git(root, ["init", "--initial-branch=main"], env));
4462
4638
  }).pipe(Effect.asVoid);
4463
- var ensureOriginRemote = (root, repoUrl) => Effect.gen(function* (_) {
4639
+ var ensureOriginRemote = (root, repoUrl, env) => Effect.gen(function* (_) {
4464
4640
  if ((yield* _(gitExitCode(root, [
4465
4641
  "remote",
4466
4642
  "set-url",
4467
4643
  "origin",
4468
4644
  repoUrl
4469
- ], gitBaseEnv$1))) === successExitCode) return;
4645
+ ], env))) === successExitCode) return;
4470
4646
  yield* _(git(root, [
4471
4647
  "remote",
4472
4648
  "add",
4473
4649
  "origin",
4474
4650
  repoUrl
4475
- ], gitBaseEnv$1));
4651
+ ], env));
4476
4652
  });
4477
- var checkoutBranchBestEffort = (root, repoRef) => Effect.gen(function* (_) {
4653
+ var checkoutBranchBestEffort = (root, repoRef, env) => Effect.gen(function* (_) {
4478
4654
  const checkoutExit = yield* _(gitExitCode(root, [
4479
4655
  "checkout",
4480
4656
  "-B",
4481
4657
  repoRef
4482
- ], gitBaseEnv$1));
4658
+ ], env));
4483
4659
  if (checkoutExit === successExitCode) return;
4484
4660
  yield* _(Effect.logWarning(`git checkout -B ${repoRef} failed (exit ${checkoutExit})`));
4485
4661
  });
4486
- var stateInit = (input) => Effect.gen(function* (_) {
4487
- const fs = yield* _(FileSystem.FileSystem);
4488
- const path = yield* _(Path.Path);
4489
- const root = resolveStateRoot(path, process.cwd());
4490
- yield* _(initRepoIfNeeded(fs, path, root, input));
4491
- yield* _(ensureOriginRemote(root, input.repoUrl));
4492
- yield* _(checkoutBranchBestEffort(root, input.repoRef));
4493
- yield* _(ensureStateGitignore(fs, path, root));
4494
- yield* _(Effect.log(`State dir ready: ${root}`));
4495
- yield* _(Effect.log(`Remote: ${input.repoUrl}`));
4496
- }).pipe(Effect.asVoid);
4662
+ var stateInit = (input) => {
4663
+ const doInit = (env) => Effect.gen(function* (_) {
4664
+ const fs = yield* _(FileSystem.FileSystem);
4665
+ const path = yield* _(Path.Path);
4666
+ const root = resolveStateRoot(path, process.cwd());
4667
+ yield* _(initRepoIfNeeded(fs, path, root, input, env));
4668
+ yield* _(ensureOriginRemote(root, input.repoUrl, env));
4669
+ yield* _(adoptRemoteHistoryIfOrphan(root, input.repoRef, env));
4670
+ yield* _(checkoutBranchBestEffort(root, input.repoRef, env));
4671
+ yield* _(ensureStateGitignore(fs, path, root));
4672
+ yield* _(Effect.log(`State dir ready: ${root}`));
4673
+ yield* _(Effect.log(`Remote: ${input.repoUrl}`));
4674
+ }).pipe(Effect.asVoid);
4675
+ const token = input.token?.trim() ?? "";
4676
+ return token.length > 0 && isGithubHttpsRemote(input.repoUrl) ? withGithubAskpassEnv(token, doInit) : doInit(gitBaseEnv$1);
4677
+ };
4497
4678
  var stateStatus = Effect.gen(function* (_) {
4498
- const output = yield* _(gitCapture(resolveStateRoot(yield* _(Path.Path), process.cwd()), [
4679
+ const output = yield* _(gitCapture$1(resolveStateRoot(yield* _(Path.Path), process.cwd()), [
4499
4680
  "status",
4500
4681
  "-sb",
4501
4682
  "--porcelain=v1"
@@ -4514,7 +4695,7 @@ var statePull = Effect.gen(function* (_) {
4514
4695
  yield* _(git(root, ["pull", "--rebase"], gitBaseEnv$1));
4515
4696
  return;
4516
4697
  }
4517
- const originUrl = yield* _(gitCapture(root, [
4698
+ const originUrl = yield* _(gitCapture$1(root, [
4518
4699
  "remote",
4519
4700
  "get-url",
4520
4701
  "origin"
@@ -4539,13 +4720,13 @@ var statePush = Effect.gen(function* (_) {
4539
4720
  ], gitBaseEnv$1));
4540
4721
  return;
4541
4722
  }
4542
- const originUrl = yield* _(gitCapture(root, [
4723
+ const originUrl = yield* _(gitCapture$1(root, [
4543
4724
  "remote",
4544
4725
  "get-url",
4545
4726
  "origin"
4546
4727
  ], gitBaseEnv$1).pipe(Effect.map((value) => value.trim())));
4547
4728
  const token = yield* _(resolveGithubToken(fs, path, root));
4548
- yield* _(token && token.length > 0 && isGithubHttpsRemote(originUrl) ? withGithubAskpassEnv(token, (env) => pipe(gitCapture(root, [
4729
+ yield* _(token && token.length > 0 && isGithubHttpsRemote(originUrl) ? withGithubAskpassEnv(token, (env) => pipe(gitCapture$1(root, [
4549
4730
  "rev-parse",
4550
4731
  "--abbrev-ref",
4551
4732
  "HEAD"
@@ -5105,18 +5286,18 @@ var ensureAvailableSshPort = (projectDir, config) => Effect.gen(function* (_) {
5105
5286
  });
5106
5287
  var runDockerComposeUpWithPortCheck = (projectDir) => Effect.gen(function* (_) {
5107
5288
  const config = yield* _(readProjectConfig(projectDir));
5108
- const updated = (yield* _(runDockerComposePsFormatted(projectDir).pipe(Effect.map((raw) => parseComposePsOutput(raw)), Effect.map((rows) => rows.length > 0)))) ? config.template : yield* _(ensureAvailableSshPort(projectDir, config));
5109
- yield* _(syncManagedProjectFiles(projectDir, updated));
5110
- yield* _(ensureComposeNetworkReady(projectDir, updated));
5289
+ const resolvedTemplate = yield* _(resolveTemplateResourceLimits((yield* _(runDockerComposePsFormatted(projectDir).pipe(Effect.map((raw) => parseComposePsOutput(raw)), Effect.map((rows) => rows.length > 0)))) ? config.template : yield* _(ensureAvailableSshPort(projectDir, config))));
5290
+ yield* _(syncManagedProjectFiles(projectDir, resolvedTemplate));
5291
+ yield* _(ensureComposeNetworkReady(projectDir, resolvedTemplate));
5111
5292
  yield* _(runDockerComposeUp(projectDir));
5112
- yield* _(ensureClaudeCliReady(projectDir, updated.containerName));
5293
+ yield* _(ensureClaudeCliReady(projectDir, resolvedTemplate.containerName));
5113
5294
  const ensureBridgeAccess = (containerName) => runDockerInspectContainerBridgeIp(projectDir, containerName).pipe(Effect.flatMap((bridgeIp) => bridgeIp.length > 0 ? Effect.void : runDockerNetworkConnectBridge(projectDir, containerName)), Effect.matchEffect({
5114
5295
  onFailure: (error) => Effect.logWarning(`Failed to connect ${containerName} to bridge network: ${error instanceof Error ? error.message : String(error)}`),
5115
5296
  onSuccess: () => Effect.void
5116
5297
  }));
5117
- yield* _(ensureBridgeAccess(updated.containerName));
5118
- if (updated.enableMcpPlaywright) yield* _(ensureBridgeAccess(`${updated.containerName}-browser`));
5119
- return updated;
5298
+ yield* _(ensureBridgeAccess(resolvedTemplate.containerName));
5299
+ if (resolvedTemplate.enableMcpPlaywright) yield* _(ensureBridgeAccess(`${resolvedTemplate.containerName}-browser`));
5300
+ return resolvedTemplate;
5120
5301
  });
5121
5302
  //#endregion
5122
5303
  //#region ../lib/src/usecases/projects-ssh.ts
@@ -5408,7 +5589,7 @@ var resolveRootedConfig = (command, ctx) => ({
5408
5589
  codexAuthPath: ctx.resolveRootPath(command.config.codexAuthPath),
5409
5590
  codexSharedAuthPath: ctx.resolveRootPath(command.config.codexSharedAuthPath)
5410
5591
  });
5411
- var resolveCreateConfig = (command, ctx, resolvedOutDir) => resolveSshPort(resolveRootedConfig(command, ctx), resolvedOutDir).pipe(Effect.flatMap((config) => applyGithubForkConfig(config)));
5592
+ var resolveCreateConfig = (command, ctx, resolvedOutDir) => resolveSshPort(resolveRootedConfig(command, ctx), resolvedOutDir).pipe(Effect.flatMap((config) => applyGithubForkConfig(config)), Effect.flatMap((config) => resolveTemplateResourceLimits(config)));
5412
5593
  var logCreatedProject = (resolvedOutDir, createdFiles) => Effect.gen(function* (_) {
5413
5594
  yield* _(Effect.log(`Created docker-git project in ${resolvedOutDir}`));
5414
5595
  for (const file of createdFiles) yield* _(Effect.log(` - ${file}`));
@@ -5524,7 +5705,7 @@ var normalizeAuthLabel = (value) => {
5524
5705
  };
5525
5706
  //#endregion
5526
5707
  //#region ../lib/src/usecases/apply-overrides.ts
5527
- var hasApplyOverrides = (command) => command.gitTokenLabel !== void 0 || command.codexTokenLabel !== void 0 || command.claudeTokenLabel !== void 0 || command.enableMcpPlaywright !== void 0;
5708
+ var hasApplyOverrides = (command) => command.gitTokenLabel !== void 0 || command.codexTokenLabel !== void 0 || command.claudeTokenLabel !== void 0 || command.cpuLimit !== void 0 || command.ramLimit !== void 0 || command.enableMcpPlaywright !== void 0;
5528
5709
  var applyTemplateOverrides = (template, command) => {
5529
5710
  if (command === void 0) return template;
5530
5711
  let nextTemplate = template;
@@ -5540,6 +5721,14 @@ var applyTemplateOverrides = (template, command) => {
5540
5721
  ...nextTemplate,
5541
5722
  claudeAuthLabel: normalizeAuthLabel(command.claudeTokenLabel)
5542
5723
  };
5724
+ if (command.cpuLimit !== void 0) nextTemplate = {
5725
+ ...nextTemplate,
5726
+ cpuLimit: command.cpuLimit
5727
+ };
5728
+ if (command.ramLimit !== void 0) nextTemplate = {
5729
+ ...nextTemplate,
5730
+ ramLimit: command.ramLimit
5731
+ };
5543
5732
  if (command.enableMcpPlaywright !== void 0) nextTemplate = {
5544
5733
  ...nextTemplate,
5545
5734
  enableMcpPlaywright: command.enableMcpPlaywright
@@ -5547,22 +5736,12 @@ var applyTemplateOverrides = (template, command) => {
5547
5736
  return nextTemplate;
5548
5737
  };
5549
5738
  //#endregion
5550
- //#region ../lib/src/usecases/apply.ts
5551
- var applyProjectFiles = (projectDir, command) => Effect.gen(function* (_) {
5552
- yield* _(Effect.log(`Applying docker-git config files in ${projectDir}...`));
5553
- const resolvedTemplate = applyTemplateOverrides((yield* _(readProjectConfig(projectDir))).template, command);
5554
- yield* _(writeProjectFiles(projectDir, resolvedTemplate, true));
5555
- yield* _(ensureCodexConfigFile(projectDir, resolvedTemplate.codexAuthPath));
5556
- yield* _(ensureClaudeAuthSeedFromHome(defaultProjectsRoot(projectDir), ".orch/auth/claude"));
5557
- return resolvedTemplate;
5558
- });
5739
+ //#region ../lib/src/usecases/apply-project-discovery.ts
5559
5740
  var gitSuccessExitCode = 0;
5560
- var gitBranchDetached = "HEAD";
5561
- var maxLocalConfigSearchDepth = 6;
5562
5741
  var gitBaseEnv = { GIT_TERMINAL_PROMPT: "0" };
5563
5742
  var emptyConfigPaths = () => [];
5564
5743
  var nullProjectCandidate = () => null;
5565
- var nullString = () => null;
5744
+ var nullString$1 = () => null;
5566
5745
  var normalizeRepoIdentity = (repoUrl) => {
5567
5746
  const github = parseGithubRepoUrl(repoUrl);
5568
5747
  if (github !== null) {
@@ -5630,7 +5809,7 @@ var tryGitCapture = (cwd, args) => {
5630
5809
  _tag: "ApplyGitCaptureError",
5631
5810
  code
5632
5811
  })).pipe(Effect.map((value) => value.trim()), Effect.match({
5633
- onFailure: nullString,
5812
+ onFailure: nullString$1,
5634
5813
  onSuccess: (value) => value
5635
5814
  })) : Effect.succeed(null)
5636
5815
  }));
@@ -5680,6 +5859,20 @@ var collectRemoteIdentities = (repoRoot) => Effect.gen(function* (_) {
5680
5859
  }
5681
5860
  return [...identityMap.values()];
5682
5861
  });
5862
+ var gitCapture = tryGitCapture;
5863
+ //#endregion
5864
+ //#region ../lib/src/usecases/apply.ts
5865
+ var applyProjectFiles = (projectDir, command) => Effect.gen(function* (_) {
5866
+ yield* _(Effect.log(`Applying docker-git config files in ${projectDir}...`));
5867
+ const resolvedTemplate = yield* _(resolveTemplateResourceLimits(applyTemplateOverrides((yield* _(readProjectConfig(projectDir))).template, command)));
5868
+ yield* _(writeProjectFiles(projectDir, resolvedTemplate, true));
5869
+ yield* _(ensureCodexConfigFile(projectDir, resolvedTemplate.codexAuthPath));
5870
+ yield* _(ensureClaudeAuthSeedFromHome(defaultProjectsRoot(projectDir), ".orch/auth/claude"));
5871
+ return resolvedTemplate;
5872
+ });
5873
+ var gitBranchDetached = "HEAD";
5874
+ var maxLocalConfigSearchDepth = 6;
5875
+ var nullString = () => null;
5683
5876
  var resolveFromCurrentTree = () => Effect.gen(function* (_) {
5684
5877
  const { fs, path, resolved } = yield* _(resolveBaseDir("."));
5685
5878
  const configPath = yield* _(findExistingUpwards(fs, path, resolved, "docker-git.json", maxLocalConfigSearchDepth).pipe(Effect.match({
@@ -5695,11 +5888,11 @@ var normalizeBranch = (branch) => {
5695
5888
  };
5696
5889
  var resolveFromCurrentRepository = () => Effect.gen(function* (_) {
5697
5890
  const cwd = process.cwd();
5698
- const repoRoot = yield* _(tryGitCapture(cwd, ["rev-parse", "--show-toplevel"]));
5891
+ const repoRoot = yield* _(gitCapture(cwd, ["rev-parse", "--show-toplevel"]));
5699
5892
  if (repoRoot === null) return null;
5700
5893
  const remoteIdentities = yield* _(collectRemoteIdentities(repoRoot));
5701
5894
  if (remoteIdentities.length === 0) return null;
5702
- const branch = normalizeBranch(yield* _(tryGitCapture(repoRoot, [
5895
+ const branch = normalizeBranch(yield* _(gitCapture(repoRoot, [
5703
5896
  "rev-parse",
5704
5897
  "--abbrev-ref",
5705
5898
  "HEAD"
@@ -6156,6 +6349,87 @@ var authCodexStatus = (command) => withCodexAuth(command, ({ accountPath, cwd })
6156
6349
  }));
6157
6350
  var authCodexLogout = (command) => withCodexAuth(command, ({ accountPath, cwd }) => runCodexLogout(cwd, accountPath)).pipe(Effect.zipRight(autoSyncState(`chore(state): auth codex logout ${normalizeAccountLabel(command.label, "default")}`)));
6158
6351
  //#endregion
6352
+ //#region ../lib/src/usecases/state-repo-github.ts
6353
+ var dotDockerGitRepoName = ".docker-git";
6354
+ var defaultStateRef = "main";
6355
+ var resolveViewerLogin = (cwd, hostPath, token) => Effect.gen(function* (_) {
6356
+ const raw = yield* _(runGhApiCapture(cwd, hostPath, token, [
6357
+ "/user",
6358
+ "--jq",
6359
+ ".login"
6360
+ ]));
6361
+ if (raw.length === 0) return yield* _(Effect.fail(new CommandFailedError({
6362
+ command: "gh api /user --jq .login",
6363
+ exitCode: 1
6364
+ })));
6365
+ return raw;
6366
+ });
6367
+ var getRepoCloneUrl = (cwd, hostPath, token, login) => runGhApiNullable(cwd, hostPath, token, [
6368
+ `/repos/${login}/${dotDockerGitRepoName}`,
6369
+ "--jq",
6370
+ ".clone_url"
6371
+ ]);
6372
+ var createStateRepo = (cwd, hostPath, token) => runGhApiNullable(cwd, hostPath, token, [
6373
+ "-X",
6374
+ "POST",
6375
+ "/user/repos",
6376
+ "-f",
6377
+ `name=${dotDockerGitRepoName}`,
6378
+ "-f",
6379
+ "private=false",
6380
+ "-f",
6381
+ "auto_init=true",
6382
+ "--jq",
6383
+ ".clone_url"
6384
+ ]);
6385
+ /**
6386
+ * Ensures the .docker-git state repository exists on GitHub and is initialised locally.
6387
+ *
6388
+ * On GitHub auth, immediately:
6389
+ * 1. Resolve the authenticated user's login via the GitHub API
6390
+ * 2. Check whether `<login>/.docker-git` exists on GitHub
6391
+ * 3. If missing, create the repository (public, auto-initialised with a README)
6392
+ * 4. Initialise the local `~/.docker-git` directory as a clone of that repository
6393
+ *
6394
+ * All failures are swallowed and logged as warnings so they never abort the auth
6395
+ * flow itself.
6396
+ *
6397
+ * @param token - A valid GitHub personal-access or OAuth token
6398
+ * @returns Effect<void, never, GithubStateRepoRuntime>
6399
+ *
6400
+ * @pure false
6401
+ * @effect FileSystem, CommandExecutor (Docker gh CLI, git)
6402
+ * @invariant ∀token ∈ ValidTokens: ensureStateDotDockerGitRepo(token) → cloned(~/.docker-git) ∨ warned
6403
+ * @precondition token.length > 0
6404
+ * @postcondition ~/.docker-git is a git repo with origin pointing to github.com/<login>/.docker-git
6405
+ * @complexity O(1) API calls
6406
+ * @throws Never - all errors are caught and logged
6407
+ */
6408
+ var ensureStateDotDockerGitRepo = (token) => withFsPathContext(({ cwd, fs, path }) => Effect.gen(function* (_) {
6409
+ const ghRoot = resolvePathFromCwd(path, cwd, ghAuthRoot);
6410
+ yield* _(fs.makeDirectory(ghRoot, { recursive: true }));
6411
+ yield* _(ensureGhAuthImage(fs, path, cwd, "gh api"));
6412
+ const login = yield* _(resolveViewerLogin(cwd, ghRoot, token));
6413
+ let cloneUrl = yield* _(getRepoCloneUrl(cwd, ghRoot, token, login));
6414
+ if (cloneUrl === null) {
6415
+ yield* _(Effect.log(`Creating .docker-git repository for ${login}...`));
6416
+ cloneUrl = yield* _(createStateRepo(cwd, ghRoot, token));
6417
+ }
6418
+ if (cloneUrl === null) {
6419
+ yield* _(Effect.logWarning(`Could not resolve or create .docker-git repository for ${login}`));
6420
+ return;
6421
+ }
6422
+ yield* _(Effect.log(`Initializing state repository: ${cloneUrl}`));
6423
+ yield* _(stateInit({
6424
+ repoUrl: cloneUrl,
6425
+ repoRef: defaultStateRef,
6426
+ token
6427
+ }));
6428
+ })).pipe(Effect.matchEffect({
6429
+ onFailure: (error) => Effect.logWarning(`State repo setup failed: ${error instanceof Error ? error.message : String(error)}`),
6430
+ onSuccess: () => Effect.void
6431
+ }));
6432
+ //#endregion
6159
6433
  //#region ../lib/src/usecases/auth-github.ts
6160
6434
  var ensureGithubOrchLayout = (cwd, envGlobalPath) => migrateLegacyOrchLayout(cwd, {
6161
6435
  envGlobalPath,
@@ -6253,6 +6527,7 @@ var runGithubInteractiveLogin = (cwd, fs, path, envPath, command) => Effect.gen(
6253
6527
  const resolved = yield* _(resolveGithubTokenFromGh(cwd, accountPath));
6254
6528
  yield* _(ensureEnvFile$1(fs, path, envPath));
6255
6529
  yield* _(persistGithubToken(fs, envPath, buildGithubTokenKey(command.label), resolved));
6530
+ return resolved;
6256
6531
  });
6257
6532
  var authGithubLogin = (command) => withFsPathContext(({ cwd, fs, path }) => Effect.gen(function* (_) {
6258
6533
  yield* _(ensureGithubOrchLayout(cwd, command.envGlobalPath));
@@ -6263,10 +6538,11 @@ var authGithubLogin = (command) => withFsPathContext(({ cwd, fs, path }) => Effe
6263
6538
  if (token.length > 0) {
6264
6539
  yield* _(ensureEnvFile$1(fs, path, envPath));
6265
6540
  yield* _(persistGithubToken(fs, envPath, key, token));
6541
+ yield* _(ensureStateDotDockerGitRepo(token));
6266
6542
  yield* _(autoSyncState(`chore(state): auth gh ${label}`));
6267
6543
  return;
6268
6544
  }
6269
- yield* _(runGithubInteractiveLogin(cwd, fs, path, envPath, command));
6545
+ yield* _(ensureStateDotDockerGitRepo(yield* _(runGithubInteractiveLogin(cwd, fs, path, envPath, command))));
6270
6546
  yield* _(autoSyncState(`chore(state): auth gh ${label}`));
6271
6547
  }));
6272
6548
  var authGithubStatus = (command) => withEnvContext(command.envGlobalPath, ({ current, envPath }) => Effect.gen(function* (_) {
@@ -6951,6 +7227,22 @@ var valueOptionSpecByFlag = new Map([
6951
7227
  flag: "--codex-home",
6952
7228
  key: "codexHome"
6953
7229
  },
7230
+ {
7231
+ flag: "--cpu",
7232
+ key: "cpuLimit"
7233
+ },
7234
+ {
7235
+ flag: "--cpus",
7236
+ key: "cpuLimit"
7237
+ },
7238
+ {
7239
+ flag: "--ram",
7240
+ key: "ramLimit"
7241
+ },
7242
+ {
7243
+ flag: "--memory",
7244
+ key: "ramLimit"
7245
+ },
6954
7246
  {
6955
7247
  flag: "--network-mode",
6956
7248
  key: "dockerNetworkMode"
@@ -7127,6 +7419,14 @@ var valueFlagUpdaters = {
7127
7419
  ...raw,
7128
7420
  codexHome: value
7129
7421
  }),
7422
+ cpuLimit: (raw, value) => ({
7423
+ ...raw,
7424
+ cpuLimit: value
7425
+ }),
7426
+ ramLimit: (raw, value) => ({
7427
+ ...raw,
7428
+ ramLimit: value
7429
+ }),
7130
7430
  dockerNetworkMode: (raw, value) => ({
7131
7431
  ...raw,
7132
7432
  dockerNetworkMode: value
@@ -7304,15 +7604,22 @@ var parseProjectDirWithOptions = (args, defaultProjectDir = ".") => Either.gen(f
7304
7604
  var parseProjectDirArgs = (args, defaultProjectDir = ".") => Either.map(parseProjectDirWithOptions(args, defaultProjectDir), ({ projectDir }) => ({ projectDir }));
7305
7605
  //#endregion
7306
7606
  //#region src/docker-git/cli/parser-apply.ts
7307
- var parseApply = (args) => Either.map(parseProjectDirWithOptions(args), ({ projectDir, raw }) => ({
7308
- _tag: "Apply",
7309
- projectDir,
7310
- runUp: raw.up ?? true,
7311
- gitTokenLabel: raw.gitTokenLabel,
7312
- codexTokenLabel: raw.codexTokenLabel,
7313
- claudeTokenLabel: raw.claudeTokenLabel,
7314
- enableMcpPlaywright: raw.enableMcpPlaywright
7315
- }));
7607
+ var parseApply = (args) => Either.gen(function* (_) {
7608
+ const { projectDir, raw } = yield* _(parseProjectDirWithOptions(args));
7609
+ const cpuLimit = yield* _(normalizeCpuLimit(raw.cpuLimit, "--cpu"));
7610
+ const ramLimit = yield* _(normalizeRamLimit(raw.ramLimit, "--ram"));
7611
+ return {
7612
+ _tag: "Apply",
7613
+ projectDir,
7614
+ runUp: raw.up ?? true,
7615
+ gitTokenLabel: raw.gitTokenLabel,
7616
+ codexTokenLabel: raw.codexTokenLabel,
7617
+ claudeTokenLabel: raw.claudeTokenLabel,
7618
+ cpuLimit,
7619
+ ramLimit,
7620
+ enableMcpPlaywright: raw.enableMcpPlaywright
7621
+ };
7622
+ });
7316
7623
  //#endregion
7317
7624
  //#region src/docker-git/cli/parser-attach.ts
7318
7625
  var parseAttach = (args) => {
@@ -7419,7 +7726,7 @@ var resolveAutoAgentFlags = (raw) => {
7419
7726
  });
7420
7727
  };
7421
7728
  //#endregion
7422
- //#region ../lib/src/core/command-builders.ts
7729
+ //#region ../lib/src/core/command-builders-shared.ts
7423
7730
  var parsePort = (value) => {
7424
7731
  const parsed = Number(value);
7425
7732
  if (!Number.isInteger(parsed)) return Either.left({
@@ -7434,6 +7741,7 @@ var parsePort = (value) => {
7434
7741
  });
7435
7742
  return Either.right(parsed);
7436
7743
  };
7744
+ var parseSshPort = (value) => parsePort(value);
7437
7745
  var parseDockerNetworkMode = (value) => {
7438
7746
  const candidate = value?.trim() ?? defaultTemplateConfig.dockerNetworkMode;
7439
7747
  if (isDockerNetworkMode(candidate)) return Either.right(candidate);
@@ -7451,6 +7759,8 @@ var nonEmpty = (option, value, fallback) => {
7451
7759
  });
7452
7760
  return Either.right(candidate);
7453
7761
  };
7762
+ //#endregion
7763
+ //#region ../lib/src/core/command-builders.ts
7454
7764
  var normalizeSecretsRoot = (value) => trimRightChar(value, "/");
7455
7765
  var resolveRepoBasics = (raw) => Either.gen(function* (_) {
7456
7766
  const resolvedRepo = resolveRepoInput(raw.repoUrl?.trim() ?? "");
@@ -7470,7 +7780,7 @@ var resolveRepoBasics = (raw) => Either.gen(function* (_) {
7470
7780
  repoRef,
7471
7781
  targetDir: expandContainerHome(sshUser, yield* _(nonEmpty("--target-dir", raw.targetDir, defaultTemplateConfig.targetDir))),
7472
7782
  sshUser,
7473
- sshPort: yield* _(parsePort(raw.sshPort ?? String(defaultTemplateConfig.sshPort)))
7783
+ sshPort: yield* _(parseSshPort(raw.sshPort ?? String(defaultTemplateConfig.sshPort)))
7474
7784
  };
7475
7785
  });
7476
7786
  var resolveNames = (raw, projectSlug) => Either.gen(function* (_) {
@@ -7525,7 +7835,7 @@ var resolveCreateBehavior = (raw) => ({
7525
7835
  forceEnv: raw.forceEnv ?? false,
7526
7836
  enableMcpPlaywright: raw.enableMcpPlaywright ?? false
7527
7837
  });
7528
- var buildTemplateConfig = ({ agentAuto, agentMode, claudeAuthLabel, codexAuthLabel, dockerNetworkMode, dockerSharedNetworkName, enableMcpPlaywright, gitTokenLabel, names, paths, repo }) => ({
7838
+ var buildTemplateConfig = ({ agentAuto, agentMode, claudeAuthLabel, codexAuthLabel, cpuLimit, dockerNetworkMode, dockerSharedNetworkName, enableMcpPlaywright, gitTokenLabel, names, paths, ramLimit, repo }) => ({
7529
7839
  containerName: names.containerName,
7530
7840
  serviceName: names.serviceName,
7531
7841
  sshUser: repo.sshUser,
@@ -7544,6 +7854,8 @@ var buildTemplateConfig = ({ agentAuto, agentMode, claudeAuthLabel, codexAuthLab
7544
7854
  codexAuthPath: paths.codexAuthPath,
7545
7855
  codexSharedAuthPath: paths.codexSharedAuthPath,
7546
7856
  codexHome: paths.codexHome,
7857
+ cpuLimit,
7858
+ ramLimit,
7547
7859
  dockerNetworkMode,
7548
7860
  dockerSharedNetworkName,
7549
7861
  enableMcpPlaywright,
@@ -7559,6 +7871,8 @@ var buildCreateCommand = (raw) => Either.gen(function* (_) {
7559
7871
  const gitTokenLabel = normalizeGitTokenLabel(raw.gitTokenLabel);
7560
7872
  const codexAuthLabel = normalizeAuthLabel(raw.codexTokenLabel);
7561
7873
  const claudeAuthLabel = normalizeAuthLabel(raw.claudeTokenLabel);
7874
+ const cpuLimit = yield* _(normalizeCpuLimit(raw.cpuLimit ?? "30%", "--cpu"));
7875
+ const ramLimit = yield* _(normalizeRamLimit(raw.ramLimit ?? "30%", "--ram"));
7562
7876
  const dockerNetworkMode = yield* _(parseDockerNetworkMode(raw.dockerNetworkMode));
7563
7877
  const dockerSharedNetworkName = yield* _(nonEmpty("--shared-network", raw.dockerSharedNetworkName, defaultTemplateConfig.dockerSharedNetworkName));
7564
7878
  const { agentAuto, agentMode } = yield* _(resolveAutoAgentFlags(raw));
@@ -7574,6 +7888,8 @@ var buildCreateCommand = (raw) => Either.gen(function* (_) {
7574
7888
  repo,
7575
7889
  names,
7576
7890
  paths,
7891
+ cpuLimit,
7892
+ ramLimit,
7577
7893
  dockerNetworkMode,
7578
7894
  dockerSharedNetworkName,
7579
7895
  gitTokenLabel,
@@ -7856,6 +8172,8 @@ Options:
7856
8172
  --env-project <path> Host path to project env file (default: ./.orch/env/project.env)
7857
8173
  --codex-auth <path> Host path for Codex auth cache (default: <projectsRoot>/.orch/auth/codex)
7858
8174
  --codex-home <path> Container path for Codex auth (default: /home/dev/.codex)
8175
+ --cpu <value> CPU limit: percent or cores (examples: 30%, 1.5; default: 30%)
8176
+ --ram <value> RAM limit: percent or size (examples: 30%, 512m, 4g; default: 30%)
7859
8177
  --network-mode <mode> Compose network mode: shared|project (default: shared)
7860
8178
  --shared-network <name> Shared Docker network name when network-mode=shared (default: docker-git-shared)
7861
8179
  --out-dir <path> Output directory (default: <projectsRoot>/<org>/<repo>[/issue-<id>|/pr-<id>])
@@ -8348,6 +8666,8 @@ var createSteps = [
8348
8666
  "repoUrl",
8349
8667
  "repoRef",
8350
8668
  "outDir",
8669
+ "cpuLimit",
8670
+ "ramLimit",
8351
8671
  "runUp",
8352
8672
  "mcpPlaywright",
8353
8673
  "force"
@@ -8400,15 +8720,38 @@ var menuItems = [
8400
8720
  ];
8401
8721
  //#endregion
8402
8722
  //#region src/docker-git/menu-create.ts
8723
+ var optionalCreateArgs = (input) => [
8724
+ {
8725
+ value: input.repoUrl,
8726
+ args: ["--repo-url", input.repoUrl]
8727
+ },
8728
+ {
8729
+ value: input.repoRef,
8730
+ args: ["--repo-ref", input.repoRef]
8731
+ },
8732
+ {
8733
+ value: input.outDir,
8734
+ args: ["--out-dir", input.outDir]
8735
+ },
8736
+ {
8737
+ value: input.cpuLimit,
8738
+ args: ["--cpu", input.cpuLimit]
8739
+ },
8740
+ {
8741
+ value: input.ramLimit,
8742
+ args: ["--ram", input.ramLimit]
8743
+ }
8744
+ ];
8745
+ var booleanCreateFlags = (input) => [
8746
+ input.runUp ? null : "--no-up",
8747
+ input.enableMcpPlaywright ? "--mcp-playwright" : null,
8748
+ input.force ? "--force" : null,
8749
+ input.forceEnv ? "--force-env" : null
8750
+ ].filter((value) => value !== null);
8403
8751
  var buildCreateArgs = (input) => {
8404
8752
  const args = ["create"];
8405
- if (input.repoUrl.length > 0) args.push("--repo-url", input.repoUrl);
8406
- if (input.repoRef.length > 0) args.push("--repo-ref", input.repoRef);
8407
- if (input.outDir.length > 0) args.push("--out-dir", input.outDir);
8408
- if (!input.runUp) args.push("--no-up");
8409
- if (input.enableMcpPlaywright) args.push("--mcp-playwright");
8410
- if (input.force) args.push("--force");
8411
- if (input.forceEnv) args.push("--force-env");
8753
+ for (const spec of optionalCreateArgs(input)) if (spec.value.length > 0) args.push(spec.args[0], spec.args[1]);
8754
+ for (const flag of booleanCreateFlags(input)) args.push(flag);
8412
8755
  return args;
8413
8756
  };
8414
8757
  var trimLeftSlash = (value) => {
@@ -8441,6 +8784,8 @@ var resolveCreateInputs = (cwd, values) => {
8441
8784
  repoUrl,
8442
8785
  repoRef: values.repoRef ?? resolvedRepoRef ?? "main",
8443
8786
  outDir,
8787
+ cpuLimit: values.cpuLimit ?? "",
8788
+ ramLimit: values.ramLimit ?? "",
8444
8789
  runUp: values.runUp !== false,
8445
8790
  enableMcpPlaywright: values.enableMcpPlaywright === true,
8446
8791
  force: values.force === true,
@@ -8484,6 +8829,12 @@ var applyCreateStep = (input) => Match.value(input.step).pipe(Match.when("repoUr
8484
8829
  }), Match.when("outDir", () => {
8485
8830
  input.nextValues.outDir = input.buffer.length > 0 ? input.buffer : input.currentDefaults.outDir;
8486
8831
  return true;
8832
+ }), Match.when("cpuLimit", () => {
8833
+ input.nextValues.cpuLimit = input.buffer.length > 0 ? input.buffer : input.currentDefaults.cpuLimit;
8834
+ return true;
8835
+ }), Match.when("ramLimit", () => {
8836
+ input.nextValues.ramLimit = input.buffer.length > 0 ? input.buffer : input.currentDefaults.ramLimit;
8837
+ return true;
8487
8838
  }), Match.when("runUp", () => {
8488
8839
  input.nextValues.runUp = parseYesDefault(input.buffer, input.currentDefaults.runUp);
8489
8840
  return true;
@@ -10106,7 +10457,7 @@ var renderProjectAuthPrompt = (view, message) => {
10106
10457
  };
10107
10458
  //#endregion
10108
10459
  //#region src/docker-git/menu-render.ts
10109
- var renderStepLabel = (step, defaults) => Match.value(step).pipe(Match.when("repoUrl", () => "Repo URL (optional for empty workspace)"), Match.when("repoRef", () => `Repo ref [${defaults.repoRef}]`), Match.when("outDir", () => `Output dir [${defaults.outDir}]`), Match.when("runUp", () => `Run docker compose up now? [${defaults.runUp ? "Y" : "n"}]`), Match.when("mcpPlaywright", () => `Enable Playwright MCP (Chromium sidecar)? [${defaults.enableMcpPlaywright ? "y" : "N"}]`), Match.when("force", () => `Force recreate (overwrite files + wipe volumes)? [${defaults.force ? "y" : "N"}]`), Match.exhaustive);
10460
+ var renderStepLabel = (step, defaults) => Match.value(step).pipe(Match.when("repoUrl", () => "Repo URL (optional for empty workspace)"), Match.when("repoRef", () => `Repo ref [${defaults.repoRef}]`), Match.when("outDir", () => `Output dir [${defaults.outDir}]`), Match.when("cpuLimit", () => `CPU limit [${defaults.cpuLimit || "30%"}]`), Match.when("ramLimit", () => `RAM limit [${defaults.ramLimit || "30%"}]`), Match.when("runUp", () => `Run docker compose up now? [${defaults.runUp ? "Y" : "n"}]`), Match.when("mcpPlaywright", () => `Enable Playwright MCP (Chromium sidecar)? [${defaults.enableMcpPlaywright ? "y" : "N"}]`), Match.when("force", () => `Force recreate (overwrite files + wipe volumes)? [${defaults.force ? "y" : "N"}]`), Match.exhaustive);
10110
10461
  var compactElements = (items) => items.filter((item) => item !== null);
10111
10462
  var renderMenuHints = (el) => el(Box, {
10112
10463
  marginTop: 1,