@prover-coder-ai/docker-git 1.0.5

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