@prover-coder-ai/docker-git 1.0.21 → 1.0.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/.package.json.release.bak +5 -3
  2. package/CHANGELOG.md +12 -0
  3. package/README.md +31 -1
  4. package/dist/src/docker-git/main.js +10259 -12
  5. package/dist/src/docker-git/main.js.map +1 -0
  6. package/package.json +4 -4
  7. package/src/app/program.ts +16 -13
  8. package/src/docker-git/cli/parser-options.ts +6 -0
  9. package/src/docker-git/cli/parser.ts +1 -0
  10. package/src/docker-git/cli/usage.ts +9 -4
  11. package/src/docker-git/menu-actions.ts +5 -2
  12. package/src/docker-git/menu-create.ts +9 -13
  13. package/src/docker-git/menu-render.ts +1 -1
  14. package/tests/docker-git/parser-helpers.ts +76 -0
  15. package/tests/docker-git/parser-network-options.test.ts +47 -0
  16. package/tests/docker-git/parser.test.ts +30 -71
  17. package/vite.docker-git.config.ts +34 -0
  18. package/dist/main.js +0 -930
  19. package/dist/main.js.map +0 -1
  20. package/dist/src/app/main.js +0 -15
  21. package/dist/src/app/program.js +0 -61
  22. package/dist/src/docker-git/cli/input.js +0 -21
  23. package/dist/src/docker-git/cli/parser-apply.js +0 -22
  24. package/dist/src/docker-git/cli/parser-attach.js +0 -19
  25. package/dist/src/docker-git/cli/parser-auth.js +0 -90
  26. package/dist/src/docker-git/cli/parser-clone.js +0 -40
  27. package/dist/src/docker-git/cli/parser-create.js +0 -1
  28. package/dist/src/docker-git/cli/parser-mcp-playwright.js +0 -18
  29. package/dist/src/docker-git/cli/parser-options.js +0 -134
  30. package/dist/src/docker-git/cli/parser-panes.js +0 -19
  31. package/dist/src/docker-git/cli/parser-scrap.js +0 -74
  32. package/dist/src/docker-git/cli/parser-sessions.js +0 -69
  33. package/dist/src/docker-git/cli/parser-shared.js +0 -26
  34. package/dist/src/docker-git/cli/parser-state.js +0 -62
  35. package/dist/src/docker-git/cli/parser.js +0 -47
  36. package/dist/src/docker-git/cli/read-command.js +0 -17
  37. package/dist/src/docker-git/cli/usage.js +0 -113
  38. package/dist/src/docker-git/menu-actions.js +0 -135
  39. package/dist/src/docker-git/menu-auth-data.js +0 -90
  40. package/dist/src/docker-git/menu-auth-helpers.js +0 -20
  41. package/dist/src/docker-git/menu-auth.js +0 -159
  42. package/dist/src/docker-git/menu-buffer-input.js +0 -9
  43. package/dist/src/docker-git/menu-create.js +0 -199
  44. package/dist/src/docker-git/menu-input-handler.js +0 -109
  45. package/dist/src/docker-git/menu-input-utils.js +0 -47
  46. package/dist/src/docker-git/menu-input.js +0 -2
  47. package/dist/src/docker-git/menu-labeled-env.js +0 -33
  48. package/dist/src/docker-git/menu-menu.js +0 -46
  49. package/dist/src/docker-git/menu-project-auth-claude.js +0 -43
  50. package/dist/src/docker-git/menu-project-auth-data.js +0 -165
  51. package/dist/src/docker-git/menu-project-auth.js +0 -124
  52. package/dist/src/docker-git/menu-render-auth.js +0 -45
  53. package/dist/src/docker-git/menu-render-common.js +0 -26
  54. package/dist/src/docker-git/menu-render-layout.js +0 -14
  55. package/dist/src/docker-git/menu-render-project-auth.js +0 -37
  56. package/dist/src/docker-git/menu-render-select.js +0 -129
  57. package/dist/src/docker-git/menu-render.js +0 -137
  58. package/dist/src/docker-git/menu-select-actions.js +0 -66
  59. package/dist/src/docker-git/menu-select-connect.js +0 -6
  60. package/dist/src/docker-git/menu-select-load.js +0 -12
  61. package/dist/src/docker-git/menu-select-order.js +0 -21
  62. package/dist/src/docker-git/menu-select-runtime.js +0 -82
  63. package/dist/src/docker-git/menu-select-view.js +0 -15
  64. package/dist/src/docker-git/menu-select.js +0 -98
  65. package/dist/src/docker-git/menu-shared.js +0 -180
  66. package/dist/src/docker-git/menu-startup.js +0 -57
  67. package/dist/src/docker-git/menu-types.js +0 -21
  68. package/dist/src/docker-git/menu.js +0 -226
  69. package/dist/src/docker-git/program.js +0 -43
  70. package/dist/src/docker-git/tmux.js +0 -176
package/dist/main.js DELETED
@@ -1,930 +0,0 @@
1
- import { NodeContext, NodeRuntime } from "@effect/platform-node";
2
- import { Data, Match, Effect, pipe, Either, Duration, Console } from "effect";
3
- import * as CommandExecutor from "@effect/platform/CommandExecutor";
4
- import { ExitCode } from "@effect/platform/CommandExecutor";
5
- import * as Path from "@effect/platform/Path";
6
- import * as Command from "@effect/platform/Command";
7
- import * as Chunk from "effect/Chunk";
8
- import * as Stream from "effect/Stream";
9
- import * as ParseResult from "@effect/schema/ParseResult";
10
- import * as Schema from "@effect/schema/Schema";
11
- import * as TreeFormatter from "@effect/schema/TreeFormatter";
12
- import * as FileSystem from "@effect/platform/FileSystem";
13
- const emptyRequest = { _tag: "None" };
14
- const toCloneRequest = (args) => ({
15
- _tag: "Clone",
16
- args
17
- });
18
- const resolveCloneRequest = (argv, npmLifecycleEvent) => {
19
- if (npmLifecycleEvent === "clone") {
20
- if (argv.length > 0) {
21
- const [first, ...rest] = argv;
22
- return first === "clone" ? toCloneRequest(rest) : toCloneRequest(argv);
23
- }
24
- return toCloneRequest([]);
25
- }
26
- if (argv.length > 0 && argv[0] === "clone") {
27
- return toCloneRequest(argv.slice(1));
28
- }
29
- return emptyRequest;
30
- };
31
- class FileExistsError extends Data.TaggedError("FileExistsError") {
32
- }
33
- class ConfigNotFoundError extends Data.TaggedError("ConfigNotFoundError") {
34
- }
35
- class ConfigDecodeError extends Data.TaggedError("ConfigDecodeError") {
36
- }
37
- class InputCancelledError extends Data.TaggedError("InputCancelledError") {
38
- }
39
- class InputReadError extends Data.TaggedError("InputReadError") {
40
- }
41
- class DockerCommandError extends Data.TaggedError("DockerCommandError") {
42
- }
43
- class DockerAccessError extends Data.TaggedError("DockerAccessError") {
44
- }
45
- class CloneFailedError extends Data.TaggedError("CloneFailedError") {
46
- }
47
- class PortProbeError extends Data.TaggedError("PortProbeError") {
48
- }
49
- class CommandFailedError extends Data.TaggedError("CommandFailedError") {
50
- }
51
- class AuthError extends Data.TaggedError("AuthError") {
52
- }
53
- class ScrapArchiveNotFoundError extends Data.TaggedError("ScrapArchiveNotFoundError") {
54
- }
55
- class ScrapArchiveInvalidError extends Data.TaggedError("ScrapArchiveInvalidError") {
56
- }
57
- class ScrapTargetDirUnsupportedError extends Data.TaggedError("ScrapTargetDirUnsupportedError") {
58
- }
59
- class ScrapWipeRefusedError extends Data.TaggedError("ScrapWipeRefusedError") {
60
- }
61
- const trimLeftChar = (value, char) => {
62
- let start = 0;
63
- while (start < value.length && value[start] === char) {
64
- start += 1;
65
- }
66
- return value.slice(start);
67
- };
68
- const trimRightChar = (value, char) => {
69
- let end = value.length;
70
- while (end > 0 && value[end - 1] === char) {
71
- end -= 1;
72
- }
73
- return value.slice(0, end);
74
- };
75
- const slugify = (value) => {
76
- const normalized = value.trim().toLowerCase().replaceAll(/[^a-z0-9_-]+/g, "-").replaceAll(/-+/g, "-");
77
- const withoutLeading = trimLeftChar(normalized, "-");
78
- const cleaned = trimRightChar(withoutLeading, "-");
79
- return cleaned.length > 0 ? cleaned : "app";
80
- };
81
- const deriveRepoSlug = (repoUrl) => {
82
- const trimmed = trimRightChar(repoUrl.trim(), "/");
83
- if (trimmed.length === 0) {
84
- return "app";
85
- }
86
- const lastSlash = trimmed.lastIndexOf("/");
87
- const lastColon = trimmed.lastIndexOf(":");
88
- const pivot = Math.max(lastSlash, lastColon);
89
- const segment = pivot >= 0 ? trimmed.slice(pivot + 1) : trimmed;
90
- const withoutGit = segment.endsWith(".git") ? segment.slice(0, -4) : segment;
91
- return slugify(withoutGit);
92
- };
93
- const stripGitSuffix = (segment) => segment.endsWith(".git") ? segment.slice(0, -4) : segment;
94
- const normalizePathParts = (pathPart) => {
95
- const cleaned = trimLeftChar(pathPart, "/");
96
- if (cleaned.length === 0) {
97
- return [];
98
- }
99
- const rawParts = cleaned.split("/").filter(Boolean);
100
- return rawParts.map((part, index) => index === rawParts.length - 1 ? stripGitSuffix(part) : part);
101
- };
102
- const extractFromScheme = (trimmed) => {
103
- const schemeIndex = trimmed.indexOf("://");
104
- if (schemeIndex === -1) {
105
- return null;
106
- }
107
- const afterScheme = trimmed.slice(schemeIndex + 3);
108
- const firstSlash = afterScheme.indexOf("/");
109
- if (firstSlash === -1) {
110
- return [];
111
- }
112
- return normalizePathParts(afterScheme.slice(firstSlash + 1));
113
- };
114
- const extractFromColon = (trimmed) => {
115
- const colonIndex = trimmed.indexOf(":");
116
- if (colonIndex === -1) {
117
- return null;
118
- }
119
- return normalizePathParts(trimmed.slice(colonIndex + 1));
120
- };
121
- const extractFromSlash = (trimmed) => {
122
- const slashIndex = trimmed.indexOf("/");
123
- if (slashIndex === -1) {
124
- return null;
125
- }
126
- return normalizePathParts(trimmed.slice(slashIndex + 1));
127
- };
128
- const extractRepoPathParts = (repoUrl) => {
129
- const trimmed = trimRightChar(repoUrl.trim(), "/");
130
- if (trimmed.length === 0) {
131
- return [];
132
- }
133
- const fromScheme = extractFromScheme(trimmed);
134
- if (fromScheme !== null) {
135
- return fromScheme;
136
- }
137
- const fromColon = extractFromColon(trimmed);
138
- if (fromColon !== null) {
139
- return fromColon;
140
- }
141
- const fromSlash = extractFromSlash(trimmed);
142
- if (fromSlash !== null) {
143
- return fromSlash;
144
- }
145
- return [stripGitSuffix(trimmed)];
146
- };
147
- const normalizeRepoSegment = (segment, fallback) => {
148
- const normalized = slugify(segment);
149
- return normalized.length > 0 ? normalized : fallback;
150
- };
151
- const deriveRepoPathParts = (repoUrl) => {
152
- const repoSlug = deriveRepoSlug(repoUrl);
153
- const rawParts = extractRepoPathParts(repoUrl);
154
- if (rawParts.length === 0) {
155
- return { ownerParts: [], repo: repoSlug, pathParts: [repoSlug] };
156
- }
157
- const rawRepo = rawParts.at(-1) ?? repoSlug;
158
- const repo = normalizeRepoSegment(rawRepo, repoSlug);
159
- const ownerParts = rawParts.slice(0, -1).map((part) => normalizeRepoSegment(part, "org")).filter((part) => part.length > 0);
160
- const pathParts = ownerParts.length > 0 ? [...ownerParts, repo] : [repo];
161
- return { ownerParts, repo, pathParts };
162
- };
163
- const defaultTemplateConfig = {
164
- dockerGitPath: "./.docker-git",
165
- envGlobalPath: "./.docker-git/.orch/env/global.env",
166
- envProjectPath: "./.orch/env/project.env",
167
- codexSharedAuthPath: "./.docker-git/.orch/auth/codex",
168
- enableMcpPlaywright: false
169
- };
170
- const formatParseError = (error) => Match.value(error).pipe(Match.when({ _tag: "UnknownCommand" }, ({ command }) => `Unknown command: ${command}`), Match.when({ _tag: "UnknownOption" }, ({ option }) => `Unknown option: ${option}`), Match.when({ _tag: "MissingOptionValue" }, ({ option }) => `Missing value for option: ${option}`), Match.when({ _tag: "MissingRequiredOption" }, ({ option }) => `Missing required option: ${option}`), Match.when({ _tag: "InvalidOption" }, ({ option, reason }) => `Invalid option ${option}: ${reason}`), Match.when({ _tag: "UnexpectedArgument" }, ({ value }) => `Unexpected argument: ${value}`), Match.exhaustive);
171
- const buildCommand = (spec, stdout, stderr, stdin = "pipe") => pipe(Command.make(spec.command, ...spec.args), Command.workingDirectory(spec.cwd), spec.env ? Command.env(spec.env) : (value) => value, Command.stdin(stdin), Command.stdout(stdout), Command.stderr(stderr));
172
- const ensureExitCode = (exitCode, okExitCodes, onFailure) => okExitCodes.includes(exitCode) ? Effect.succeed(exitCode) : Effect.fail(onFailure(exitCode));
173
- const runCommandWithExitCodes = (spec, okExitCodes, onFailure) => Effect.gen(function* (_) {
174
- const exitCode = yield* _(Command.exitCode(buildCommand(spec, "inherit", "inherit", "inherit")));
175
- const numericExitCode = Number(exitCode);
176
- yield* _(ensureExitCode(numericExitCode, okExitCodes, onFailure));
177
- });
178
- const runCommandExitCode = (spec) => Effect.map(Command.exitCode(buildCommand(spec, "pipe", "pipe", "inherit")), Number);
179
- const collectUint8Array = (chunks) => Chunk.reduce(chunks, new Uint8Array(), (acc, curr) => {
180
- const next = new Uint8Array(acc.length + curr.length);
181
- next.set(acc);
182
- next.set(curr, acc.length);
183
- return next;
184
- });
185
- const runCommandCapture = (spec, okExitCodes, onFailure) => Effect.scoped(Effect.gen(function* (_) {
186
- const executor = yield* _(CommandExecutor.CommandExecutor);
187
- const process2 = yield* _(executor.start(buildCommand(spec, "pipe", "pipe", "pipe")));
188
- const bytes = yield* _(pipe(process2.stdout, Stream.runCollect, Effect.map((chunks) => collectUint8Array(chunks))));
189
- const exitCode = yield* _(process2.exitCode);
190
- yield* _(ensureExitCode(Number(exitCode), okExitCodes, onFailure));
191
- return new TextDecoder("utf-8").decode(bytes);
192
- }));
193
- const successExitCode$1 = Number(ExitCode(0));
194
- const readCloneRequest = Effect.sync(() => resolveCloneRequest(process.argv.slice(2), process.env["npm_lifecycle_event"]));
195
- const runDockerGitClone = (args) => Effect.gen(function* (_) {
196
- const path = yield* _(Path.Path);
197
- const workspaceRoot = process.cwd();
198
- const appRoot = path.join(workspaceRoot, "packages", "app");
199
- const dockerGitCli = path.join(appRoot, "dist", "src", "docker-git", "main.js");
200
- const buildLabel = `pnpm -C ${appRoot} build:docker-git`;
201
- const cloneLabel = `node ${dockerGitCli} clone`;
202
- yield* _(runCommandWithExitCodes({ cwd: workspaceRoot, command: "pnpm", args: ["-C", appRoot, "build:docker-git"] }, [successExitCode$1], (exitCode) => new CommandFailedError({ command: buildLabel, exitCode })));
203
- yield* _(runCommandWithExitCodes({ cwd: workspaceRoot, command: "node", args: [dockerGitCli, "clone", ...args] }, [successExitCode$1], (exitCode) => new CommandFailedError({ command: cloneLabel, exitCode })));
204
- });
205
- const resolveBaseDir = (baseDir) => Effect.gen(function* (_) {
206
- const fs = yield* _(FileSystem.FileSystem);
207
- const path = yield* _(Path.Path);
208
- const resolved = path.resolve(baseDir);
209
- return { fs, path, resolved };
210
- });
211
- const TemplateConfigSchema = Schema.Struct({
212
- containerName: Schema.String,
213
- serviceName: Schema.String,
214
- sshUser: Schema.String,
215
- sshPort: Schema.Number.pipe(Schema.int()),
216
- repoUrl: Schema.String,
217
- repoRef: Schema.String,
218
- gitTokenLabel: Schema.optional(Schema.String),
219
- codexAuthLabel: Schema.optional(Schema.String),
220
- claudeAuthLabel: Schema.optional(Schema.String),
221
- targetDir: Schema.String,
222
- volumeName: Schema.String,
223
- dockerGitPath: Schema.optionalWith(Schema.String, {
224
- default: () => defaultTemplateConfig.dockerGitPath
225
- }),
226
- authorizedKeysPath: Schema.String,
227
- envGlobalPath: Schema.optionalWith(Schema.String, {
228
- default: () => defaultTemplateConfig.envGlobalPath
229
- }),
230
- envProjectPath: Schema.optionalWith(Schema.String, {
231
- default: () => defaultTemplateConfig.envProjectPath
232
- }),
233
- codexAuthPath: Schema.String,
234
- codexSharedAuthPath: Schema.optionalWith(Schema.String, {
235
- default: () => defaultTemplateConfig.codexSharedAuthPath
236
- }),
237
- codexHome: Schema.String,
238
- enableMcpPlaywright: Schema.optionalWith(Schema.Boolean, {
239
- default: () => defaultTemplateConfig.enableMcpPlaywright
240
- }),
241
- pnpmVersion: Schema.String
242
- });
243
- const ProjectConfigSchema = Schema.Struct({
244
- schemaVersion: Schema.Literal(1),
245
- template: TemplateConfigSchema
246
- });
247
- const ProjectConfigJsonSchema = Schema.parseJson(ProjectConfigSchema);
248
- const decodeProjectConfig = (path, input) => Either.match(ParseResult.decodeUnknownEither(ProjectConfigJsonSchema)(input), {
249
- onLeft: (issue) => Effect.fail(new ConfigDecodeError({
250
- path,
251
- message: TreeFormatter.formatIssueSync(issue)
252
- })),
253
- onRight: (value) => Effect.succeed(value)
254
- });
255
- const readProjectConfig = (baseDir) => Effect.gen(function* (_) {
256
- const { fs, path, resolved } = yield* _(resolveBaseDir(baseDir));
257
- const configPath = path.join(resolved, "docker-git.json");
258
- const exists = yield* _(fs.exists(configPath));
259
- if (!exists) {
260
- return yield* _(Effect.fail(new ConfigNotFoundError({ path: configPath })));
261
- }
262
- const contents = yield* _(fs.readFileString(configPath));
263
- return yield* _(decodeProjectConfig(configPath, contents));
264
- });
265
- const composeSpec = (cwd, args) => ({
266
- cwd,
267
- command: "docker",
268
- args: ["compose", "--ansi", "never", "--progress", "plain", ...args]
269
- });
270
- const runCompose = (cwd, args, okExitCodes) => runCommandWithExitCodes(composeSpec(cwd, args), okExitCodes, (exitCode) => new DockerCommandError({ exitCode }));
271
- const runComposeCapture = (cwd, args, okExitCodes) => runCommandCapture(composeSpec(cwd, args), okExitCodes, (exitCode) => new DockerCommandError({ exitCode }));
272
- const runDockerComposeDown = (cwd) => runCompose(cwd, ["down"], [Number(ExitCode(0))]);
273
- const runDockerComposePsFormatted = (cwd) => runComposeCapture(cwd, ["ps", "--format", "{{.Name}} {{.Status}} {{.Ports}} {{.Image}}"], [Number(ExitCode(0))]);
274
- const runDockerPsNames = (cwd) => pipe(runCommandCapture({
275
- cwd,
276
- command: "docker",
277
- args: ["ps", "--format", "{{.Names}}"]
278
- }, [Number(ExitCode(0))], (exitCode) => new CommandFailedError({ command: "docker ps", exitCode })), Effect.map((output) => output.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0)));
279
- const isParseError = (error) => error._tag === "UnknownCommand" || error._tag === "UnknownOption" || error._tag === "MissingOptionValue" || error._tag === "MissingRequiredOption" || error._tag === "InvalidOption" || error._tag === "UnexpectedArgument";
280
- const renderDockerAccessHeadline = (issue) => issue === "PermissionDenied" ? "Cannot access Docker daemon socket: permission denied." : "Cannot connect to Docker daemon.";
281
- const renderDockerAccessActionPlan = (issue) => {
282
- const permissionDeniedPlan = [
283
- "Action plan:",
284
- "1) In the same shell, run: `groups $USER` and make sure group `docker` is present.",
285
- "2) Re-login to refresh group memberships and run command again.",
286
- "3) If DOCKER_HOST is set to rootless socket, keep running: `export DOCKER_HOST=unix:///run/user/$UID/docker.sock`.",
287
- "4) If using a dedicated socket not in /run/user, set DOCKER_HOST explicitly and re-run.",
288
- "Tip: this app now auto-tries a rootless socket fallback on first permission error."
289
- ];
290
- const daemonUnavailablePlan = [
291
- "Action plan:",
292
- "1) Check daemon status: `systemctl --user status docker` or `systemctl status docker`.",
293
- "2) Start daemon: `systemctl --user start docker` (or `systemctl start docker` for system Docker).",
294
- "3) Retry command in a new shell."
295
- ];
296
- return issue === "PermissionDenied" ? permissionDeniedPlan.join("\n") : daemonUnavailablePlan.join("\n");
297
- };
298
- const renderPrimaryError = (error) => Match.value(error).pipe(Match.when({ _tag: "FileExistsError" }, ({ path }) => `File already exists: ${path} (use --force to overwrite)`), Match.when({ _tag: "DockerCommandError" }, ({ exitCode }) => [
299
- `docker compose failed with exit code ${exitCode}`,
300
- "Hint: ensure Docker daemon is running and current user can access /var/run/docker.sock (for example via the docker group).",
301
- "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."
302
- ].join("\n")), Match.when({ _tag: "DockerAccessError" }, ({ details, issue }) => [
303
- renderDockerAccessHeadline(issue),
304
- "Hint: ensure Docker daemon is running and current user can access the docker socket.",
305
- "Hint: if you use rootless Docker, set DOCKER_HOST to your user socket (for example unix:///run/user/$UID/docker.sock).",
306
- renderDockerAccessActionPlan(issue),
307
- `Details: ${details}`
308
- ].join("\n")), Match.when({ _tag: "CloneFailedError" }, ({ repoRef, repoUrl, targetDir }) => `Clone failed for ${repoUrl} (${repoRef}) into ${targetDir}`), Match.when({ _tag: "PortProbeError" }, ({ message, port }) => `SSH port check failed for ${port}: ${message}`), Match.when({ _tag: "CommandFailedError" }, ({ command, exitCode }) => `${command} failed with exit code ${exitCode}`), Match.when({ _tag: "ScrapArchiveNotFoundError" }, ({ path }) => `Scrap archive not found: ${path} (run docker-git scrap export first)`), Match.when({ _tag: "ScrapArchiveInvalidError" }, ({ message, path }) => `Invalid scrap archive: ${path}
309
- Details: ${message}`), Match.when({ _tag: "ScrapTargetDirUnsupportedError" }, ({ reason, targetDir }) => [
310
- `Cannot use scrap with targetDir ${targetDir}.`,
311
- `Reason: ${reason}`,
312
- `Hint: scrap currently supports workspaces under the ssh home directory only (for example: ~/repo).`
313
- ].join("\n")), Match.when({ _tag: "ScrapWipeRefusedError" }, ({ reason, targetDir }) => [
314
- `Refusing to wipe workspace for scrap import (targetDir ${targetDir}).`,
315
- `Reason: ${reason}`,
316
- "Hint: re-run with --no-wipe, or set a narrower --target-dir when creating the project."
317
- ].join("\n")), Match.when({ _tag: "AuthError" }, ({ message }) => message), Match.orElse(() => null));
318
- const renderConfigError = (error) => {
319
- if (error._tag === "ConfigNotFoundError") {
320
- return `docker-git.json not found: ${error.path} (run docker-git create in that directory)`;
321
- }
322
- if (error._tag === "ConfigDecodeError") {
323
- return `Invalid docker-git.json at ${error.path}: ${error.message}`;
324
- }
325
- return null;
326
- };
327
- const renderInputError = (error) => {
328
- if (error._tag === "InputCancelledError") {
329
- return "Input cancelled.";
330
- }
331
- if (error._tag === "InputReadError") {
332
- return `Input error: ${error.message}`;
333
- }
334
- return null;
335
- };
336
- const renderNonParseError = (error) => renderPrimaryError(error) ?? renderConfigError(error) ?? renderInputError(error) ?? error.message;
337
- const renderError = (error) => {
338
- if (isParseError(error)) {
339
- return formatParseError(error);
340
- }
341
- return renderNonParseError(error);
342
- };
343
- const splitLines = (input) => input.replaceAll("\r\n", "\n").replaceAll("\r", "\n").split("\n");
344
- const isAlpha = (char) => {
345
- const code = char.codePointAt(0) ?? 0;
346
- return code >= 65 && code <= 90 || code >= 97 && code <= 122;
347
- };
348
- const isDigit = (char) => {
349
- const code = char.codePointAt(0) ?? 0;
350
- return code >= 48 && code <= 57;
351
- };
352
- const isValidFirstChar = (char) => isAlpha(char) || char === "_";
353
- const isValidEnvChar = (char) => isAlpha(char) || isDigit(char) || char === "_";
354
- const hasOnlyValidChars = (value) => {
355
- for (const char of value) {
356
- if (!isValidEnvChar(char)) {
357
- return false;
358
- }
359
- }
360
- return true;
361
- };
362
- const isEnvKey = (value) => {
363
- if (value.length === 0) {
364
- return false;
365
- }
366
- const first = value[0] ?? "";
367
- if (!isValidFirstChar(first)) {
368
- return false;
369
- }
370
- return hasOnlyValidChars(value.slice(1));
371
- };
372
- const parseEnvLine = (line) => {
373
- const trimmed = line.trim();
374
- if (trimmed.length === 0 || trimmed.startsWith("#")) {
375
- return null;
376
- }
377
- const raw = trimmed.startsWith("export ") ? trimmed.slice("export ".length).trimStart() : trimmed;
378
- const eqIndex = raw.indexOf("=");
379
- if (eqIndex <= 0) {
380
- return null;
381
- }
382
- const key = raw.slice(0, eqIndex).trim();
383
- if (!isEnvKey(key)) {
384
- return null;
385
- }
386
- const value = raw.slice(eqIndex + 1).trim();
387
- return { key, value };
388
- };
389
- const parseEnvEntries = (input) => {
390
- const entries = [];
391
- for (const line of splitLines(input)) {
392
- const parsed = parseEnvLine(line);
393
- if (parsed) {
394
- entries.push(parsed);
395
- }
396
- }
397
- return entries;
398
- };
399
- const resolveAuthorizedKeysPath = (path, baseDir, authorizedKeysPath) => path.isAbsolute(authorizedKeysPath) ? authorizedKeysPath : path.resolve(baseDir, authorizedKeysPath);
400
- const resolveHomeDir = () => {
401
- const raw = process.env["HOME"] ?? process.env["USERPROFILE"];
402
- const home = raw?.trim() ?? "";
403
- return home.length > 0 ? home : null;
404
- };
405
- const expandHome = (value, home) => {
406
- if (home === null) {
407
- return value;
408
- }
409
- if (value === "~") {
410
- return home;
411
- }
412
- if (value.startsWith("~/") || value.startsWith("~\\")) {
413
- return `${home}${value.slice(1)}`;
414
- }
415
- return value;
416
- };
417
- const trimTrailingSlash = (value) => {
418
- let end = value.length;
419
- while (end > 0) {
420
- const char = value[end - 1];
421
- if (char !== "/" && char !== "\\") {
422
- break;
423
- }
424
- end -= 1;
425
- }
426
- return value.slice(0, end);
427
- };
428
- const defaultProjectsRoot = (cwd) => {
429
- const home = resolveHomeDir();
430
- const explicit = process.env["DOCKER_GIT_PROJECTS_ROOT"]?.trim();
431
- if (explicit && explicit.length > 0) {
432
- return expandHome(explicit, home);
433
- }
434
- if (home !== null) {
435
- return `${trimTrailingSlash(home)}/.docker-git`;
436
- }
437
- return `${cwd}/.docker-git`;
438
- };
439
- const normalizeRelativePath = (value) => value.replaceAll("\\", "/").replace(/^\.\//, "").trim();
440
- const resolvePathFromCwd = (path, cwd, targetPath) => path.isAbsolute(targetPath) ? targetPath : (() => {
441
- const projectsRoot = path.resolve(defaultProjectsRoot(cwd));
442
- const normalized = normalizeRelativePath(targetPath);
443
- if (normalized === ".docker-git") {
444
- return projectsRoot;
445
- }
446
- const prefix = ".docker-git/";
447
- if (normalized.startsWith(prefix)) {
448
- return path.join(projectsRoot, normalized.slice(prefix.length));
449
- }
450
- return path.resolve(cwd, targetPath);
451
- })();
452
- const findExistingUpwards = (fs, path, startDir, fileName, maxDepth) => Effect.gen(function* (_) {
453
- let current = startDir;
454
- for (let depth = 0; depth <= maxDepth; depth += 1) {
455
- const candidate = path.join(current, fileName);
456
- const exists = yield* _(fs.exists(candidate));
457
- if (exists) {
458
- return candidate;
459
- }
460
- const parent = path.dirname(current);
461
- if (parent === current) {
462
- return null;
463
- }
464
- current = parent;
465
- }
466
- return null;
467
- });
468
- const resolveEnvPath = (key) => {
469
- const value = process.env[key]?.trim();
470
- return value && value.length > 0 ? value : null;
471
- };
472
- const findExistingPath = (fs, candidate) => candidate === null ? Effect.succeed(null) : Effect.flatMap(fs.exists(candidate), (exists) => exists ? Effect.succeed(candidate) : Effect.succeed(null));
473
- const findFirstExisting = (fs, candidates) => Effect.gen(function* (_) {
474
- for (const candidate of candidates) {
475
- const existing = yield* _(findExistingPath(fs, candidate));
476
- if (existing !== null) {
477
- return existing;
478
- }
479
- }
480
- return null;
481
- });
482
- const findKeyByPriority = (fs, path, cwd, spec) => Effect.gen(function* (_) {
483
- const envPath = resolveEnvPath(spec.envVar);
484
- const envExisting = yield* _(findExistingPath(fs, envPath));
485
- if (envExisting !== null) {
486
- return envExisting;
487
- }
488
- const devKey = yield* _(findExistingUpwards(fs, path, cwd, spec.devKeyName, 6));
489
- if (devKey !== null) {
490
- return devKey;
491
- }
492
- if (spec.fallbackName !== void 0) {
493
- const fallback = yield* _(findExistingUpwards(fs, path, cwd, spec.fallbackName, 6));
494
- if (fallback !== null) {
495
- return fallback;
496
- }
497
- }
498
- const home = resolveEnvPath("HOME");
499
- if (home === null) {
500
- return null;
501
- }
502
- return yield* _(findFirstExisting(fs, spec.homeCandidates.map((candidate) => path.join(home, ".ssh", candidate))));
503
- });
504
- const sshPrivateKeySpec = {
505
- envVar: "DOCKER_GIT_SSH_KEY",
506
- devKeyName: "dev_ssh_key",
507
- homeCandidates: ["id_ed25519", "id_rsa"]
508
- };
509
- const makeKeyFinder = (spec) => (fs, path, cwd) => findKeyByPriority(fs, path, cwd, spec);
510
- const findSshPrivateKey = makeKeyFinder(sshPrivateKeySpec);
511
- const withFsPathContext = (run) => Effect.gen(function* (_) {
512
- const fs = yield* _(FileSystem.FileSystem);
513
- const path = yield* _(Path.Path);
514
- return yield* _(run({ fs, path, cwd: process.cwd() }));
515
- });
516
- const formatConnectionInfo = (cwd, config, authorizedKeysPath, authorizedKeysExists, sshCommand) => `Project directory: ${cwd}
517
- Container: ${config.template.containerName}
518
- Service: ${config.template.serviceName}
519
- SSH command: ${sshCommand}
520
- Repo: ${config.template.repoUrl} (${config.template.repoRef})
521
- Workspace: ${config.template.targetDir}
522
- Authorized keys: ${authorizedKeysPath}${authorizedKeysExists ? "" : " (missing)"}
523
- Env global: ${config.template.envGlobalPath}
524
- Env project: ${config.template.envProjectPath}
525
- Codex auth: ${config.template.codexAuthPath} -> ${config.template.codexHome}`;
526
- const isDockerGitConfig = (entry) => entry.endsWith("docker-git.json");
527
- const shouldSkipDir = (entry) => entry === ".git" || entry === ".orch" || entry === ".docker-git" || entry === ".cache";
528
- const isNotFoundStatError = (error) => error._tag === "SystemError" && error.reason === "NotFound";
529
- const processDockerGitEntry = (fs, path, dir, entry, state) => Effect.gen(function* (_) {
530
- if (shouldSkipDir(entry)) {
531
- return;
532
- }
533
- const resolved = path.join(dir, entry);
534
- const info = yield* _(fs.stat(resolved).pipe(Effect.catchTag("SystemError", (error) => isNotFoundStatError(error) ? Effect.succeed(null) : Effect.fail(error))));
535
- if (info === null) {
536
- return;
537
- }
538
- if (info.type === "Directory") {
539
- state.stack.push(resolved);
540
- return;
541
- }
542
- if (info.type === "File" && isDockerGitConfig(entry)) {
543
- state.results.push(resolved);
544
- }
545
- }).pipe(Effect.asVoid);
546
- const findDockerGitConfigPaths = (fs, path, rootDir) => Effect.gen(function* (_) {
547
- const exists = yield* _(fs.exists(rootDir));
548
- if (!exists) {
549
- return [];
550
- }
551
- const results = [];
552
- const stack = [rootDir];
553
- const state = { stack, results };
554
- while (stack.length > 0) {
555
- const dir = stack.pop();
556
- if (dir === void 0) {
557
- break;
558
- }
559
- const entries = yield* _(fs.readDirectory(dir));
560
- for (const entry of entries) {
561
- yield* _(processDockerGitEntry(fs, path, dir, entry, state));
562
- }
563
- }
564
- return results;
565
- });
566
- const sshOptions = "-tt -Y -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null";
567
- const buildSshCommand = (config, sshKey) => sshKey === null ? `ssh ${sshOptions} -p ${config.sshPort} ${config.sshUser}@localhost` : `ssh -i ${sshKey} ${sshOptions} -p ${config.sshPort} ${config.sshUser}@localhost`;
568
- const loadProjectBase = (configPath) => Effect.gen(function* (_) {
569
- const { fs, path, resolved } = yield* _(resolveBaseDir(configPath));
570
- const projectDir = path.dirname(resolved);
571
- const config = yield* _(readProjectConfig(projectDir));
572
- return { fs, path, projectDir, config };
573
- });
574
- const findProjectConfigPaths = (projectsRoot) => withFsPathContext(({ fs, path }) => findDockerGitConfigPaths(fs, path, path.resolve(projectsRoot)));
575
- const loadProjectSummary = (configPath, sshKey) => Effect.gen(function* (_) {
576
- const { config, fs, path, projectDir } = yield* _(loadProjectBase(configPath));
577
- const resolvedAuthorizedKeys = resolveAuthorizedKeysPath(path, projectDir, config.template.authorizedKeysPath);
578
- const authExists = yield* _(fs.exists(resolvedAuthorizedKeys));
579
- const sshCommand = buildSshCommand(config.template, sshKey);
580
- return {
581
- projectDir,
582
- config,
583
- sshCommand,
584
- authorizedKeysPath: resolvedAuthorizedKeys,
585
- authorizedKeysExists: authExists
586
- };
587
- });
588
- const loadProjectStatus = (configPath) => Effect.gen(function* (_) {
589
- const { config, projectDir } = yield* _(loadProjectBase(configPath));
590
- return { projectDir, config };
591
- });
592
- const renderProjectSummary = (summary) => formatConnectionInfo(summary.projectDir, summary.config, summary.authorizedKeysPath, summary.authorizedKeysExists, summary.sshCommand);
593
- const formatDisplayName = (repoUrl) => {
594
- const parts = deriveRepoPathParts(repoUrl);
595
- if (parts.pathParts.length > 0) {
596
- return parts.pathParts.join("/");
597
- }
598
- return repoUrl;
599
- };
600
- const loadProjectItem = (configPath, sshKey) => Effect.gen(function* (_) {
601
- const { config, fs, path, projectDir } = yield* _(loadProjectBase(configPath));
602
- const template = config.template;
603
- const resolvedAuthorizedKeys = resolveAuthorizedKeysPath(path, projectDir, template.authorizedKeysPath);
604
- const authExists = yield* _(fs.exists(resolvedAuthorizedKeys));
605
- const sshCommand = buildSshCommand(template, sshKey);
606
- const displayName = formatDisplayName(template.repoUrl);
607
- return {
608
- projectDir,
609
- displayName,
610
- repoUrl: template.repoUrl,
611
- repoRef: template.repoRef,
612
- containerName: template.containerName,
613
- serviceName: template.serviceName,
614
- sshUser: template.sshUser,
615
- sshPort: template.sshPort,
616
- targetDir: template.targetDir,
617
- sshCommand,
618
- sshKeyPath: sshKey,
619
- authorizedKeysPath: resolvedAuthorizedKeys,
620
- authorizedKeysExists: authExists,
621
- envGlobalPath: resolvePathFromCwd(path, projectDir, template.envGlobalPath),
622
- envProjectPath: resolvePathFromCwd(path, projectDir, template.envProjectPath),
623
- codexAuthPath: resolvePathFromCwd(path, projectDir, template.codexAuthPath),
624
- codexHome: template.codexHome
625
- };
626
- });
627
- const renderProjectStatusHeader = (status) => `Project: ${status.projectDir}`;
628
- const skipWithWarning = (configPath) => (error) => pipe(Effect.logWarning(`Skipping ${configPath}: ${renderError(error)}`), Effect.as(null));
629
- const forEachProjectStatus = (configPaths, run) => Effect.gen(function* (_) {
630
- for (const configPath of configPaths) {
631
- const status = yield* _(loadProjectStatus(configPath).pipe(Effect.matchEffect({
632
- onFailure: skipWithWarning(configPath),
633
- onSuccess: (value) => Effect.succeed(value)
634
- })));
635
- if (status === null) {
636
- continue;
637
- }
638
- yield* _(run(status));
639
- }
640
- }).pipe(Effect.asVoid);
641
- const normalizeCell = (value) => value?.trim() ?? "-";
642
- const parseComposeLine = (line) => {
643
- const [name, status, ports, image] = line.split(" ");
644
- return {
645
- name: normalizeCell(name),
646
- status: normalizeCell(status),
647
- ports: normalizeCell(ports),
648
- image: normalizeCell(image)
649
- };
650
- };
651
- const parseComposePsOutput = (raw) => {
652
- const lines = raw.split(/\r?\n/).map((line) => line.trimEnd()).filter((line) => line.length > 0);
653
- return lines.map((line) => parseComposeLine(line));
654
- };
655
- const padRight = (value, width) => value.length >= width ? value : `${value}${" ".repeat(width - value.length)}`;
656
- const formatComposeRows = (entries) => {
657
- if (entries.length === 0) {
658
- return " status: not running";
659
- }
660
- const nameWidth = Math.min(24, Math.max(...entries.map((row) => row.name.length), "name".length));
661
- const statusWidth = Math.min(28, Math.max(...entries.map((row) => row.status.length), "status".length));
662
- const portsWidth = Math.min(28, Math.max(...entries.map((row) => row.ports.length), "ports".length));
663
- const header = ` ${padRight("name", nameWidth)} ${padRight("status", statusWidth)} ${padRight("ports", portsWidth)} image`;
664
- const lines = entries.map((row) => ` ${padRight(row.name, nameWidth)} ${padRight(row.status, statusWidth)} ${padRight(row.ports, portsWidth)} ${row.image}`);
665
- return [header, ...lines].join("\n");
666
- };
667
- const loadProjectIndex = () => Effect.gen(function* (_) {
668
- const projectsRoot = defaultProjectsRoot(process.cwd());
669
- const configPaths = yield* _(findProjectConfigPaths(projectsRoot));
670
- if (configPaths.length === 0) {
671
- yield* _(Effect.log(`No docker-git projects found in ${projectsRoot}`));
672
- return null;
673
- }
674
- return { projectsRoot, configPaths };
675
- });
676
- const withProjectIndexAndSsh = (run) => pipe(loadProjectIndex(), Effect.flatMap((index) => index === null ? Effect.succeed(null) : Effect.gen(function* (_) {
677
- const fs = yield* _(FileSystem.FileSystem);
678
- const path = yield* _(Path.Path);
679
- const sshKey = yield* _(findSshPrivateKey(fs, path, process.cwd()));
680
- return yield* _(run(index, sshKey));
681
- })));
682
- const successExitCode = Number(ExitCode(0));
683
- const gitBaseEnv = {
684
- // Avoid blocking on interactive credential prompts in CI / TUI contexts.
685
- GIT_TERMINAL_PROMPT: "0"
686
- };
687
- const git = (cwd, args, env = gitBaseEnv) => runCommandWithExitCodes({ cwd, command: "git", args, env }, [successExitCode], (exitCode) => new CommandFailedError({ command: `git ${args[0] ?? ""}`, exitCode }));
688
- const gitExitCode = (cwd, args, env = gitBaseEnv) => runCommandExitCode({ cwd, command: "git", args, env });
689
- const gitCapture = (cwd, args, env = gitBaseEnv) => runCommandCapture({ cwd, command: "git", args, env }, [successExitCode], (exitCode) => new CommandFailedError({ command: `git ${args[0] ?? ""}`, exitCode }));
690
- const githubTokenKey = "GITHUB_TOKEN";
691
- const isGithubHttpsRemote = (url) => /^https:\/\/github\.com\//.test(url.trim());
692
- const resolveTokenFromProcessEnv = () => {
693
- const github = process.env["GITHUB_TOKEN"];
694
- if (github !== void 0) {
695
- const trimmed = github.trim();
696
- if (trimmed.length > 0) {
697
- return trimmed;
698
- }
699
- }
700
- const gh = process.env["GH_TOKEN"];
701
- if (gh !== void 0) {
702
- const trimmed = gh.trim();
703
- if (trimmed.length > 0) {
704
- return trimmed;
705
- }
706
- }
707
- return null;
708
- };
709
- const findTokenInEnvEntries = (entries) => {
710
- const directEntry = entries.find((e) => e.key === githubTokenKey);
711
- if (directEntry !== void 0) {
712
- const direct = directEntry.value.trim();
713
- if (direct.length > 0) {
714
- return direct;
715
- }
716
- }
717
- const labeledEntry = entries.find((e) => e.key.startsWith("GITHUB_TOKEN__"));
718
- if (labeledEntry !== void 0) {
719
- const labeled = labeledEntry.value.trim();
720
- if (labeled.length > 0) {
721
- return labeled;
722
- }
723
- }
724
- return null;
725
- };
726
- const resolveGithubToken = (fs, path, root) => Effect.gen(function* (_) {
727
- const fromEnv = resolveTokenFromProcessEnv();
728
- if (fromEnv !== null) {
729
- return fromEnv;
730
- }
731
- const candidates = [
732
- // Canonical layout: ~/.docker-git/.orch/env/global.env
733
- path.join(root, ".orch", "env", "global.env"),
734
- // Legacy layout (kept for backward compatibility): ~/.docker-git/secrets/global.env
735
- path.join(root, "secrets", "global.env")
736
- ];
737
- for (const envPath of candidates) {
738
- const exists = yield* _(fs.exists(envPath));
739
- if (!exists) {
740
- continue;
741
- }
742
- const text = yield* _(fs.readFileString(envPath));
743
- const token = findTokenInEnvEntries(parseEnvEntries(text));
744
- if (token !== null) {
745
- return token;
746
- }
747
- }
748
- return null;
749
- });
750
- const withGithubAskpassEnv = (token, use) => Effect.scoped(Effect.gen(function* (_) {
751
- const fs = yield* _(FileSystem.FileSystem);
752
- const askpassPath = yield* _(fs.makeTempFileScoped({ prefix: "docker-git-askpass-" }));
753
- const contents = [
754
- "#!/bin/sh",
755
- 'case "$1" in',
756
- ' *Username*) echo "x-access-token" ;;',
757
- ' *Password*) echo "${DOCKER_GIT_GITHUB_TOKEN}" ;;',
758
- ' *) echo "${DOCKER_GIT_GITHUB_TOKEN}" ;;',
759
- "esac",
760
- ""
761
- ].join("\n");
762
- yield* _(fs.writeFileString(askpassPath, contents));
763
- yield* _(fs.chmod(askpassPath, 448));
764
- const env = {
765
- ...gitBaseEnv,
766
- DOCKER_GIT_GITHUB_TOKEN: token,
767
- GIT_ASKPASS: askpassPath,
768
- GIT_ASKPASS_REQUIRE: "force"
769
- };
770
- return yield* _(use(env));
771
- }));
772
- const resolveStateRoot = (path, cwd) => path.resolve(defaultProjectsRoot(cwd));
773
- Effect.gen(function* (_) {
774
- const path = yield* _(Path.Path);
775
- const cwd = process.cwd();
776
- const root = resolveStateRoot(path, cwd);
777
- yield* _(Effect.log(root));
778
- }).pipe(Effect.asVoid);
779
- Effect.gen(function* (_) {
780
- const path = yield* _(Path.Path);
781
- const root = resolveStateRoot(path, process.cwd());
782
- const output = yield* _(gitCapture(root, ["status", "-sb", "--porcelain=v1"], gitBaseEnv));
783
- yield* _(Effect.log(output.trim().length > 0 ? output.trimEnd() : "(clean)"));
784
- }).pipe(Effect.asVoid);
785
- Effect.gen(function* (_) {
786
- const fs = yield* _(FileSystem.FileSystem);
787
- const path = yield* _(Path.Path);
788
- const root = resolveStateRoot(path, process.cwd());
789
- const originUrlExit = yield* _(gitExitCode(root, ["remote", "get-url", "origin"], gitBaseEnv));
790
- if (originUrlExit !== successExitCode) {
791
- yield* _(git(root, ["pull", "--rebase"], gitBaseEnv));
792
- return;
793
- }
794
- const originUrl = yield* _(gitCapture(root, ["remote", "get-url", "origin"], gitBaseEnv).pipe(Effect.map((value) => value.trim())));
795
- const token = yield* _(resolveGithubToken(fs, path, root));
796
- const effect = token && token.length > 0 && isGithubHttpsRemote(originUrl) ? withGithubAskpassEnv(token, (env) => git(root, ["pull", "--rebase"], env)) : git(root, ["pull", "--rebase"], gitBaseEnv);
797
- yield* _(effect);
798
- }).pipe(Effect.asVoid);
799
- Effect.gen(function* (_) {
800
- const fs = yield* _(FileSystem.FileSystem);
801
- const path = yield* _(Path.Path);
802
- const root = resolveStateRoot(path, process.cwd());
803
- const originUrlExit = yield* _(gitExitCode(root, ["remote", "get-url", "origin"], gitBaseEnv));
804
- if (originUrlExit !== successExitCode) {
805
- yield* _(git(root, ["push", "-u", "origin", "HEAD"], gitBaseEnv));
806
- return;
807
- }
808
- const originUrl = yield* _(gitCapture(root, ["remote", "get-url", "origin"], gitBaseEnv).pipe(Effect.map((value) => value.trim())));
809
- const token = yield* _(resolveGithubToken(fs, path, root));
810
- const effect = token && token.length > 0 && isGithubHttpsRemote(originUrl) ? withGithubAskpassEnv(token, (env) => pipe(gitCapture(root, ["rev-parse", "--abbrev-ref", "HEAD"], env), Effect.map((value) => value.trim()), Effect.map((branch) => branch === "HEAD" ? "main" : branch), Effect.flatMap((branch) => git(root, ["push", "--no-verify", originUrl, `HEAD:refs/heads/${branch}`], env)))) : git(root, ["push", "--no-verify", "-u", "origin", "HEAD"], gitBaseEnv);
811
- yield* _(effect);
812
- }).pipe(Effect.asVoid);
813
- pipe(loadProjectIndex(), Effect.flatMap((index) => index === null ? Effect.void : forEachProjectStatus(index.configPaths, (status) => pipe(Effect.log(renderProjectStatusHeader(status)), Effect.zipRight(runDockerComposeDown(status.projectDir).pipe(Effect.catchTag("DockerCommandError", (error) => Effect.logWarning(`docker compose down failed for ${status.projectDir}: ${renderError(error)}`))))))), Effect.asVoid);
814
- const listProjects = pipe(withProjectIndexAndSsh((index, sshKey) => Effect.gen(function* (_) {
815
- const available = [];
816
- for (const configPath of index.configPaths) {
817
- const summary = yield* _(loadProjectSummary(configPath, sshKey).pipe(Effect.matchEffect({
818
- onFailure: skipWithWarning(configPath),
819
- onSuccess: (value) => Effect.succeed(value)
820
- })));
821
- if (summary !== null) {
822
- available.push(summary);
823
- }
824
- }
825
- if (available.length === 0) {
826
- yield* _(Effect.log(`No readable docker-git projects found in ${index.projectsRoot}`));
827
- return;
828
- }
829
- yield* _(Effect.log(`Found ${available.length} docker-git project(s) in ${index.projectsRoot}`));
830
- for (const summary of available) {
831
- yield* _(Effect.log(renderProjectSummary(summary)));
832
- }
833
- })), Effect.asVoid);
834
- const emptySummaries = () => [];
835
- const emptyItems = () => [];
836
- const collectProjectValues = (configPaths, sshKey, load, toValue) => Effect.gen(function* (_) {
837
- const available = [];
838
- for (const configPath of configPaths) {
839
- const value = yield* _(load(configPath, sshKey).pipe(Effect.matchEffect({
840
- onFailure: () => Effect.succeed(null),
841
- onSuccess: (item) => Effect.succeed(toValue(item))
842
- })));
843
- if (value !== null) {
844
- available.push(value);
845
- }
846
- }
847
- return available;
848
- });
849
- const listProjectValues = (load, toValue, empty) => pipe(withProjectIndexAndSsh((index, sshKey) => collectProjectValues(index.configPaths, sshKey, load, toValue)), Effect.map((values) => values ?? empty()));
850
- listProjectValues(loadProjectSummary, renderProjectSummary, emptySummaries);
851
- const listProjectItems = listProjectValues(loadProjectItem, (value) => value, emptyItems);
852
- pipe(Effect.all([listProjectItems, runDockerPsNames(process.cwd())]), Effect.map(([items, runningNames]) => items.filter((item) => runningNames.includes(item.containerName))));
853
- Effect.asVoid(withProjectIndexAndSsh((index, sshKey) => forEachProjectStatus(index.configPaths, (status) => pipe(Effect.log(renderProjectStatusHeader(status)), Effect.zipRight(Effect.log(`SSH access: ${buildSshCommand(status.config.template, sshKey)}`)), Effect.zipRight(runDockerComposePsFormatted(status.projectDir).pipe(Effect.map((raw) => parseComposePsOutput(raw)), Effect.map((rows) => formatComposeRows(rows)), Effect.flatMap((text) => Effect.log(text)), Effect.matchEffect({
854
- onFailure: (error) => Effect.logWarning(`docker compose ps failed for ${status.projectDir}: ${renderError(error)}`),
855
- onSuccess: () => Effect.void
856
- })))))));
857
- Duration.seconds(1);
858
- const ClaudeAuthStatusSchema = Schema.Struct({
859
- loggedIn: Schema.Boolean,
860
- authMethod: Schema.optional(Schema.String),
861
- apiProvider: Schema.optional(Schema.String)
862
- });
863
- Schema.parseJson(ClaudeAuthStatusSchema);
864
- const ChunkManifestSchema = Schema.Struct({
865
- original: Schema.String,
866
- originalSize: Schema.Number,
867
- parts: Schema.Array(Schema.String),
868
- splitAt: Schema.Number,
869
- partsCount: Schema.Number,
870
- createdAt: Schema.String
871
- });
872
- Schema.parseJson(ChunkManifestSchema);
873
- const SessionManifestSchema = Schema.Struct({
874
- schemaVersion: Schema.Literal(1),
875
- mode: Schema.Literal("session"),
876
- snapshotId: Schema.String,
877
- createdAtUtc: Schema.String,
878
- repo: Schema.Struct({
879
- originUrl: Schema.String,
880
- head: Schema.String,
881
- branch: Schema.String
882
- }),
883
- artifacts: Schema.Struct({
884
- worktreePatchChunks: Schema.String,
885
- codexChunks: Schema.String,
886
- codexSharedChunks: Schema.String,
887
- envGlobalFile: Schema.optionalWith(Schema.Union(Schema.String, Schema.Null), { default: () => null }),
888
- envProjectFile: Schema.optionalWith(Schema.Union(Schema.String, Schema.Null), { default: () => null })
889
- }),
890
- rebuild: Schema.optionalWith(Schema.Struct({
891
- commands: Schema.Array(Schema.String)
892
- }), { default: () => ({ commands: [] }) })
893
- });
894
- Schema.parseJson(SessionManifestSchema);
895
- const usageText = [
896
- "Usage:",
897
- " pnpm docker-git",
898
- " pnpm clone <repo-url> [ref]",
899
- " pnpm list",
900
- "",
901
- "Notes:",
902
- " - docker-git is the interactive TUI.",
903
- " - clone builds + runs docker-git clone for you."
904
- ].join("\n");
905
- const runHelp = Console.log(usageText);
906
- const runDockerGit = pipe(
907
- readCloneRequest,
908
- Effect.flatMap(
909
- (request) => Match.value(request).pipe(
910
- Match.when({ _tag: "Clone" }, ({ args }) => runDockerGitClone(args)),
911
- Match.when({ _tag: "None" }, () => runHelp),
912
- Match.exhaustive
913
- )
914
- )
915
- );
916
- const readListFlag = Effect.sync(() => {
917
- const command = process.argv.slice(2)[0] ?? "";
918
- return command === "list" || command === "ls";
919
- });
920
- const program = Effect.gen(function* (_) {
921
- const isList = yield* _(readListFlag);
922
- if (isList) {
923
- yield* _(listProjects);
924
- return;
925
- }
926
- yield* _(runDockerGit);
927
- });
928
- const main = pipe(program, Effect.provide(NodeContext.layer));
929
- NodeRuntime.runMain(main);
930
- //# sourceMappingURL=main.js.map