@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.
- package/.jscpd.json +16 -0
- package/.package.json.release.bak +109 -0
- package/CHANGELOG.md +31 -0
- package/README.md +173 -0
- package/biome.json +34 -0
- package/dist/main.js +847 -0
- package/dist/main.js.map +1 -0
- package/dist/src/app/main.js +15 -0
- package/dist/src/app/program.js +61 -0
- package/dist/src/docker-git/cli/input.js +21 -0
- package/dist/src/docker-git/cli/parser-attach.js +19 -0
- package/dist/src/docker-git/cli/parser-auth.js +70 -0
- package/dist/src/docker-git/cli/parser-clone.js +40 -0
- package/dist/src/docker-git/cli/parser-create.js +1 -0
- package/dist/src/docker-git/cli/parser-options.js +101 -0
- package/dist/src/docker-git/cli/parser-panes.js +19 -0
- package/dist/src/docker-git/cli/parser-sessions.js +69 -0
- package/dist/src/docker-git/cli/parser-shared.js +26 -0
- package/dist/src/docker-git/cli/parser-state.js +62 -0
- package/dist/src/docker-git/cli/parser.js +42 -0
- package/dist/src/docker-git/cli/read-command.js +17 -0
- package/dist/src/docker-git/cli/usage.js +99 -0
- package/dist/src/docker-git/main.js +15 -0
- package/dist/src/docker-git/menu-actions.js +115 -0
- package/dist/src/docker-git/menu-create.js +203 -0
- package/dist/src/docker-git/menu-input.js +2 -0
- package/dist/src/docker-git/menu-menu.js +46 -0
- package/dist/src/docker-git/menu-render.js +151 -0
- package/dist/src/docker-git/menu-select.js +131 -0
- package/dist/src/docker-git/menu-shared.js +111 -0
- package/dist/src/docker-git/menu-types.js +19 -0
- package/dist/src/docker-git/menu.js +237 -0
- package/dist/src/docker-git/program.js +38 -0
- package/dist/src/docker-git/tmux.js +176 -0
- package/eslint.config.mts +305 -0
- package/eslint.effect-ts-check.config.mjs +220 -0
- package/linter.config.json +33 -0
- package/package.json +63 -0
- package/src/app/main.ts +18 -0
- package/src/app/program.ts +75 -0
- package/src/docker-git/cli/input.ts +29 -0
- package/src/docker-git/cli/parser-attach.ts +22 -0
- package/src/docker-git/cli/parser-auth.ts +124 -0
- package/src/docker-git/cli/parser-clone.ts +55 -0
- package/src/docker-git/cli/parser-create.ts +3 -0
- package/src/docker-git/cli/parser-options.ts +152 -0
- package/src/docker-git/cli/parser-panes.ts +22 -0
- package/src/docker-git/cli/parser-sessions.ts +101 -0
- package/src/docker-git/cli/parser-shared.ts +51 -0
- package/src/docker-git/cli/parser-state.ts +86 -0
- package/src/docker-git/cli/parser.ts +73 -0
- package/src/docker-git/cli/read-command.ts +26 -0
- package/src/docker-git/cli/usage.ts +112 -0
- package/src/docker-git/main.ts +18 -0
- package/src/docker-git/menu-actions.ts +246 -0
- package/src/docker-git/menu-create.ts +320 -0
- package/src/docker-git/menu-input.ts +2 -0
- package/src/docker-git/menu-menu.ts +58 -0
- package/src/docker-git/menu-render.ts +327 -0
- package/src/docker-git/menu-select.ts +250 -0
- package/src/docker-git/menu-shared.ts +141 -0
- package/src/docker-git/menu-types.ts +94 -0
- package/src/docker-git/menu.ts +339 -0
- package/src/docker-git/program.ts +134 -0
- package/src/docker-git/tmux.ts +292 -0
- package/tests/app/main.test.ts +60 -0
- package/tests/docker-git/entrypoint-auth.test.ts +29 -0
- package/tests/docker-git/parser.test.ts +172 -0
- package/tsconfig.build.json +8 -0
- package/tsconfig.json +20 -0
- package/vite.config.ts +32 -0
- package/vitest.config.ts +85 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { Either } from "effect";
|
|
2
|
+
const valueOptionSpecs = [
|
|
3
|
+
{ flag: "--repo-url", key: "repoUrl" },
|
|
4
|
+
{ flag: "--repo-ref", key: "repoRef" },
|
|
5
|
+
{ flag: "--branch", key: "repoRef" },
|
|
6
|
+
{ flag: "-b", key: "repoRef" },
|
|
7
|
+
{ flag: "--target-dir", key: "targetDir" },
|
|
8
|
+
{ flag: "--ssh-port", key: "sshPort" },
|
|
9
|
+
{ flag: "--ssh-user", key: "sshUser" },
|
|
10
|
+
{ flag: "--container-name", key: "containerName" },
|
|
11
|
+
{ flag: "--service-name", key: "serviceName" },
|
|
12
|
+
{ flag: "--volume-name", key: "volumeName" },
|
|
13
|
+
{ flag: "--secrets-root", key: "secretsRoot" },
|
|
14
|
+
{ flag: "--authorized-keys", key: "authorizedKeysPath" },
|
|
15
|
+
{ flag: "--env-global", key: "envGlobalPath" },
|
|
16
|
+
{ flag: "--env-project", key: "envProjectPath" },
|
|
17
|
+
{ flag: "--codex-auth", key: "codexAuthPath" },
|
|
18
|
+
{ flag: "--codex-home", key: "codexHome" },
|
|
19
|
+
{ flag: "--label", key: "label" },
|
|
20
|
+
{ flag: "--token", key: "token" },
|
|
21
|
+
{ flag: "--scopes", key: "scopes" },
|
|
22
|
+
{ flag: "--message", key: "message" },
|
|
23
|
+
{ flag: "-m", key: "message" },
|
|
24
|
+
{ flag: "--out-dir", key: "outDir" },
|
|
25
|
+
{ flag: "--project-dir", key: "projectDir" },
|
|
26
|
+
{ flag: "--lines", key: "lines" }
|
|
27
|
+
];
|
|
28
|
+
const valueOptionSpecByFlag = new Map(valueOptionSpecs.map((spec) => [spec.flag, spec]));
|
|
29
|
+
const booleanFlagUpdaters = {
|
|
30
|
+
"--up": (raw) => ({ ...raw, up: true }),
|
|
31
|
+
"--no-up": (raw) => ({ ...raw, up: false }),
|
|
32
|
+
"--force": (raw) => ({ ...raw, force: true }),
|
|
33
|
+
"--force-env": (raw) => ({ ...raw, forceEnv: true }),
|
|
34
|
+
"--mcp-playwright": (raw) => ({ ...raw, enableMcpPlaywright: true }),
|
|
35
|
+
"--no-mcp-playwright": (raw) => ({ ...raw, enableMcpPlaywright: false }),
|
|
36
|
+
"--web": (raw) => ({ ...raw, authWeb: true }),
|
|
37
|
+
"--include-default": (raw) => ({ ...raw, includeDefault: true })
|
|
38
|
+
};
|
|
39
|
+
const valueFlagUpdaters = {
|
|
40
|
+
repoUrl: (raw, value) => ({ ...raw, repoUrl: value }),
|
|
41
|
+
repoRef: (raw, value) => ({ ...raw, repoRef: value }),
|
|
42
|
+
targetDir: (raw, value) => ({ ...raw, targetDir: value }),
|
|
43
|
+
sshPort: (raw, value) => ({ ...raw, sshPort: value }),
|
|
44
|
+
sshUser: (raw, value) => ({ ...raw, sshUser: value }),
|
|
45
|
+
containerName: (raw, value) => ({ ...raw, containerName: value }),
|
|
46
|
+
serviceName: (raw, value) => ({ ...raw, serviceName: value }),
|
|
47
|
+
volumeName: (raw, value) => ({ ...raw, volumeName: value }),
|
|
48
|
+
secretsRoot: (raw, value) => ({ ...raw, secretsRoot: value }),
|
|
49
|
+
authorizedKeysPath: (raw, value) => ({ ...raw, authorizedKeysPath: value }),
|
|
50
|
+
envGlobalPath: (raw, value) => ({ ...raw, envGlobalPath: value }),
|
|
51
|
+
envProjectPath: (raw, value) => ({ ...raw, envProjectPath: value }),
|
|
52
|
+
codexAuthPath: (raw, value) => ({ ...raw, codexAuthPath: value }),
|
|
53
|
+
codexHome: (raw, value) => ({ ...raw, codexHome: value }),
|
|
54
|
+
label: (raw, value) => ({ ...raw, label: value }),
|
|
55
|
+
token: (raw, value) => ({ ...raw, token: value }),
|
|
56
|
+
scopes: (raw, value) => ({ ...raw, scopes: value }),
|
|
57
|
+
message: (raw, value) => ({ ...raw, message: value }),
|
|
58
|
+
outDir: (raw, value) => ({ ...raw, outDir: value }),
|
|
59
|
+
projectDir: (raw, value) => ({ ...raw, projectDir: value }),
|
|
60
|
+
lines: (raw, value) => ({ ...raw, lines: value })
|
|
61
|
+
};
|
|
62
|
+
export const applyCommandBooleanFlag = (raw, token) => {
|
|
63
|
+
const updater = booleanFlagUpdaters[token];
|
|
64
|
+
return updater ? updater(raw) : null;
|
|
65
|
+
};
|
|
66
|
+
export const applyCommandValueFlag = (raw, token, value) => {
|
|
67
|
+
const valueSpec = valueOptionSpecByFlag.get(token);
|
|
68
|
+
if (valueSpec === undefined) {
|
|
69
|
+
return Either.left({ _tag: "UnknownOption", option: token });
|
|
70
|
+
}
|
|
71
|
+
const update = valueFlagUpdaters[valueSpec.key];
|
|
72
|
+
return Either.right(update(raw, value));
|
|
73
|
+
};
|
|
74
|
+
export const parseRawOptions = (args) => {
|
|
75
|
+
let index = 0;
|
|
76
|
+
let raw = {};
|
|
77
|
+
while (index < args.length) {
|
|
78
|
+
const token = args[index] ?? "";
|
|
79
|
+
const booleanApplied = applyCommandBooleanFlag(raw, token);
|
|
80
|
+
if (booleanApplied !== null) {
|
|
81
|
+
raw = booleanApplied;
|
|
82
|
+
index += 1;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (!token.startsWith("-")) {
|
|
86
|
+
return Either.left({ _tag: "UnexpectedArgument", value: token });
|
|
87
|
+
}
|
|
88
|
+
const value = args[index + 1];
|
|
89
|
+
if (value === undefined) {
|
|
90
|
+
return Either.left({ _tag: "MissingOptionValue", option: token });
|
|
91
|
+
}
|
|
92
|
+
const nextRaw = applyCommandValueFlag(raw, token, value);
|
|
93
|
+
if (Either.isLeft(nextRaw)) {
|
|
94
|
+
return Either.left(nextRaw.left);
|
|
95
|
+
}
|
|
96
|
+
raw = nextRaw.right;
|
|
97
|
+
index += 2;
|
|
98
|
+
}
|
|
99
|
+
return Either.right(raw);
|
|
100
|
+
};
|
|
101
|
+
export {} from "@effect-template/lib/core/command-options";
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Either } from "effect";
|
|
2
|
+
import {} from "@effect-template/lib/core/domain";
|
|
3
|
+
import { parseProjectDirArgs } from "./parser-shared.js";
|
|
4
|
+
// CHANGE: parse panes command into a project selection
|
|
5
|
+
// WHY: allow listing tmux panes without attaching
|
|
6
|
+
// QUOTE(ТЗ): "покажи команду ... отобразит терминалы"
|
|
7
|
+
// REF: user-request-2026-02-02-panes
|
|
8
|
+
// SOURCE: n/a
|
|
9
|
+
// FORMAT THEOREM: forall argv: parsePanes(argv) = cmd -> deterministic(cmd)
|
|
10
|
+
// PURITY: CORE
|
|
11
|
+
// EFFECT: Effect<PanesCommand, ParseError, never>
|
|
12
|
+
// INVARIANT: projectDir is never empty
|
|
13
|
+
// COMPLEXITY: O(n) where n = |argv|
|
|
14
|
+
export const parsePanes = (args) => {
|
|
15
|
+
return Either.map(parseProjectDirArgs(args), ({ projectDir }) => ({
|
|
16
|
+
_tag: "Panes",
|
|
17
|
+
projectDir
|
|
18
|
+
}));
|
|
19
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { Either, Match } from "effect";
|
|
2
|
+
import {} from "@effect-template/lib/core/domain";
|
|
3
|
+
import { parseProjectDirWithOptions } from "./parser-shared.js";
|
|
4
|
+
const defaultLines = 200;
|
|
5
|
+
const parsePositiveInt = (option, raw) => {
|
|
6
|
+
const value = Number.parseInt(raw, 10);
|
|
7
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
8
|
+
const error = {
|
|
9
|
+
_tag: "InvalidOption",
|
|
10
|
+
option,
|
|
11
|
+
reason: "expected positive integer"
|
|
12
|
+
};
|
|
13
|
+
return Either.left(error);
|
|
14
|
+
}
|
|
15
|
+
return Either.right(value);
|
|
16
|
+
};
|
|
17
|
+
const parseList = (args) => Either.map(parseProjectDirWithOptions(args), ({ projectDir, raw }) => ({
|
|
18
|
+
_tag: "SessionsList",
|
|
19
|
+
projectDir,
|
|
20
|
+
includeDefault: raw.includeDefault === true
|
|
21
|
+
}));
|
|
22
|
+
const parsePidContext = (args) => Either.gen(function* (_) {
|
|
23
|
+
const pidRaw = args[0];
|
|
24
|
+
if (!pidRaw) {
|
|
25
|
+
const error = { _tag: "MissingRequiredOption", option: "pid" };
|
|
26
|
+
return yield* _(Either.left(error));
|
|
27
|
+
}
|
|
28
|
+
const pid = yield* _(parsePositiveInt("pid", pidRaw));
|
|
29
|
+
const { projectDir, raw } = yield* _(parseProjectDirWithOptions(args.slice(1)));
|
|
30
|
+
return { pid, projectDir, raw };
|
|
31
|
+
});
|
|
32
|
+
const parseKill = (args) => Either.map(parsePidContext(args), ({ pid, projectDir }) => ({
|
|
33
|
+
_tag: "SessionsKill",
|
|
34
|
+
projectDir,
|
|
35
|
+
pid
|
|
36
|
+
}));
|
|
37
|
+
const parseLogs = (args) => Either.gen(function* (_) {
|
|
38
|
+
const { pid, projectDir, raw } = yield* _(parsePidContext(args));
|
|
39
|
+
const lines = raw.lines ? yield* _(parsePositiveInt("--lines", raw.lines)) : defaultLines;
|
|
40
|
+
return { _tag: "SessionsLogs", projectDir, pid, lines };
|
|
41
|
+
});
|
|
42
|
+
// CHANGE: parse sessions command into list/kill/logs actions
|
|
43
|
+
// WHY: surface container terminal sessions and background processes from CLI
|
|
44
|
+
// QUOTE(ТЗ): "CLI команду которая из докера вернёт запущенные терминал сессии"
|
|
45
|
+
// REF: user-request-2026-02-04-terminal-sessions
|
|
46
|
+
// SOURCE: n/a
|
|
47
|
+
// FORMAT THEOREM: forall argv: parseSessions(argv) = cmd -> deterministic(cmd)
|
|
48
|
+
// PURITY: CORE
|
|
49
|
+
// EFFECT: Effect<SessionsCommand, ParseError, never>
|
|
50
|
+
// INVARIANT: pid/lines must be positive integers
|
|
51
|
+
// COMPLEXITY: O(n) where n = |argv|
|
|
52
|
+
export const parseSessions = (args) => {
|
|
53
|
+
if (args.length === 0) {
|
|
54
|
+
return parseList(args);
|
|
55
|
+
}
|
|
56
|
+
const first = args[0] ?? "";
|
|
57
|
+
if (first.startsWith("-")) {
|
|
58
|
+
return parseList(args);
|
|
59
|
+
}
|
|
60
|
+
const rest = args.slice(1);
|
|
61
|
+
return Match.value(first).pipe(Match.when("list", () => parseList(rest)), Match.when("kill", () => parseKill(rest)), Match.when("stop", () => parseKill(rest)), Match.when("logs", () => parseLogs(rest)), Match.when("log", () => parseLogs(rest)), Match.orElse(() => {
|
|
62
|
+
const error = {
|
|
63
|
+
_tag: "InvalidOption",
|
|
64
|
+
option: "sessions",
|
|
65
|
+
reason: `unknown action ${first}`
|
|
66
|
+
};
|
|
67
|
+
return Either.left(error);
|
|
68
|
+
}));
|
|
69
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Either } from "effect";
|
|
2
|
+
import { deriveRepoPathParts, resolveRepoInput } from "@effect-template/lib/core/domain";
|
|
3
|
+
import { parseRawOptions } from "./parser-options.js";
|
|
4
|
+
export const resolveWorkspaceRepoPath = (resolvedRepo) => {
|
|
5
|
+
const baseParts = deriveRepoPathParts(resolvedRepo.repoUrl).pathParts;
|
|
6
|
+
const projectParts = resolvedRepo.workspaceSuffix ? [...baseParts, resolvedRepo.workspaceSuffix] : baseParts;
|
|
7
|
+
return projectParts.join("/");
|
|
8
|
+
};
|
|
9
|
+
export const splitPositionalRepo = (args) => {
|
|
10
|
+
const first = args[0];
|
|
11
|
+
const positionalRepoUrl = first !== undefined && !first.startsWith("-") ? first : undefined;
|
|
12
|
+
const restArgs = positionalRepoUrl ? args.slice(1) : args;
|
|
13
|
+
return { positionalRepoUrl, restArgs };
|
|
14
|
+
};
|
|
15
|
+
export const parseProjectDirWithOptions = (args, defaultProjectDir = ".") => Either.gen(function* (_) {
|
|
16
|
+
const { positionalRepoUrl, restArgs } = splitPositionalRepo(args);
|
|
17
|
+
const raw = yield* _(parseRawOptions(restArgs));
|
|
18
|
+
const rawRepoUrl = raw.repoUrl ?? positionalRepoUrl;
|
|
19
|
+
const repoPath = rawRepoUrl ? resolveWorkspaceRepoPath(resolveRepoInput(rawRepoUrl)) : null;
|
|
20
|
+
const projectDir = raw.projectDir ??
|
|
21
|
+
(repoPath
|
|
22
|
+
? `.docker-git/${repoPath}`
|
|
23
|
+
: defaultProjectDir);
|
|
24
|
+
return { projectDir, raw };
|
|
25
|
+
});
|
|
26
|
+
export const parseProjectDirArgs = (args, defaultProjectDir = ".") => Either.map(parseProjectDirWithOptions(args, defaultProjectDir), ({ projectDir }) => ({ projectDir }));
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { Either, Match } from "effect";
|
|
2
|
+
import { parseRawOptions } from "./parser-options.js";
|
|
3
|
+
const invalidStateAction = (value) => ({
|
|
4
|
+
_tag: "InvalidOption",
|
|
5
|
+
option: "state",
|
|
6
|
+
reason: `unknown action: ${value}`
|
|
7
|
+
});
|
|
8
|
+
const unexpectedArgs = (value) => Either.left({ _tag: "UnexpectedArgument", value });
|
|
9
|
+
const parseStateInit = (args) => Either.flatMap(parseRawOptions(args), (raw) => {
|
|
10
|
+
const repoUrl = raw.repoUrl?.trim();
|
|
11
|
+
if (!repoUrl || repoUrl.length === 0) {
|
|
12
|
+
return Either.left({ _tag: "MissingRequiredOption", option: "--repo-url" });
|
|
13
|
+
}
|
|
14
|
+
return Either.right({
|
|
15
|
+
_tag: "StateInit",
|
|
16
|
+
repoUrl,
|
|
17
|
+
repoRef: raw.repoRef?.trim() && raw.repoRef.trim().length > 0 ? raw.repoRef.trim() : "main"
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
const parseStateCommit = (args) => Either.flatMap(parseRawOptions(args), (raw) => {
|
|
21
|
+
const message = raw.message?.trim();
|
|
22
|
+
if (!message || message.length === 0) {
|
|
23
|
+
return Either.left({ _tag: "MissingRequiredOption", option: "--message" });
|
|
24
|
+
}
|
|
25
|
+
return Either.right({ _tag: "StateCommit", message });
|
|
26
|
+
});
|
|
27
|
+
const parseStateSync = (args) => Either.map(parseRawOptions(args), (raw) => {
|
|
28
|
+
const message = raw.message?.trim();
|
|
29
|
+
return { _tag: "StateSync", message: message && message.length > 0 ? message : null };
|
|
30
|
+
});
|
|
31
|
+
export const parseState = (args) => {
|
|
32
|
+
const action = args[0]?.trim();
|
|
33
|
+
if (!action || action.length === 0) {
|
|
34
|
+
return Either.left({ _tag: "MissingRequiredOption", option: "state <action>" });
|
|
35
|
+
}
|
|
36
|
+
const rest = args.slice(1);
|
|
37
|
+
return Match.value(action).pipe(Match.when("path", () => {
|
|
38
|
+
if (rest.length > 0) {
|
|
39
|
+
return unexpectedArgs(rest[0] ?? "");
|
|
40
|
+
}
|
|
41
|
+
const command = { _tag: "StatePath" };
|
|
42
|
+
return Either.right(command);
|
|
43
|
+
}), Match.when("init", () => parseStateInit(rest)), Match.when("pull", () => {
|
|
44
|
+
if (rest.length > 0) {
|
|
45
|
+
return unexpectedArgs(rest[0] ?? "");
|
|
46
|
+
}
|
|
47
|
+
const command = { _tag: "StatePull" };
|
|
48
|
+
return Either.right(command);
|
|
49
|
+
}), Match.when("push", () => {
|
|
50
|
+
if (rest.length > 0) {
|
|
51
|
+
return unexpectedArgs(rest[0] ?? "");
|
|
52
|
+
}
|
|
53
|
+
const command = { _tag: "StatePush" };
|
|
54
|
+
return Either.right(command);
|
|
55
|
+
}), Match.when("status", () => {
|
|
56
|
+
if (rest.length > 0) {
|
|
57
|
+
return unexpectedArgs(rest[0] ?? "");
|
|
58
|
+
}
|
|
59
|
+
const command = { _tag: "StateStatus" };
|
|
60
|
+
return Either.right(command);
|
|
61
|
+
}), Match.when("commit", () => parseStateCommit(rest)), Match.when("sync", () => parseStateSync(rest)), Match.orElse(() => Either.left(invalidStateAction(action))));
|
|
62
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Either, Match } from "effect";
|
|
2
|
+
import {} from "@effect-template/lib/core/domain";
|
|
3
|
+
import { parseAttach } from "./parser-attach.js";
|
|
4
|
+
import { parseAuth } from "./parser-auth.js";
|
|
5
|
+
import { parseClone } from "./parser-clone.js";
|
|
6
|
+
import { buildCreateCommand } from "./parser-create.js";
|
|
7
|
+
import { parseRawOptions } from "./parser-options.js";
|
|
8
|
+
import { parsePanes } from "./parser-panes.js";
|
|
9
|
+
import { parseSessions } from "./parser-sessions.js";
|
|
10
|
+
import { parseState } from "./parser-state.js";
|
|
11
|
+
import { usageText } from "./usage.js";
|
|
12
|
+
const isHelpFlag = (token) => token === "--help" || token === "-h";
|
|
13
|
+
const helpCommand = { _tag: "Help", message: usageText };
|
|
14
|
+
const menuCommand = { _tag: "Menu" };
|
|
15
|
+
const statusCommand = { _tag: "Status" };
|
|
16
|
+
const downAllCommand = { _tag: "DownAll" };
|
|
17
|
+
const parseCreate = (args) => Either.flatMap(parseRawOptions(args), buildCreateCommand);
|
|
18
|
+
// CHANGE: parse CLI arguments into a typed command
|
|
19
|
+
// WHY: enforce deterministic, pure parsing before any effects run
|
|
20
|
+
// QUOTE(ТЗ): "Надо написать CLI команду с помощью которой мы будем создавать докер образы"
|
|
21
|
+
// REF: user-request-2026-01-07
|
|
22
|
+
// SOURCE: n/a
|
|
23
|
+
// FORMAT THEOREM: forall argv: parse(argv) = cmd -> deterministic(cmd)
|
|
24
|
+
// PURITY: CORE
|
|
25
|
+
// EFFECT: Effect<Command, ParseError, never>
|
|
26
|
+
// INVARIANT: parse does not perform IO and returns the same result for same argv
|
|
27
|
+
// COMPLEXITY: O(n) where n = |argv|
|
|
28
|
+
export const parseArgs = (args) => {
|
|
29
|
+
if (args.length === 0) {
|
|
30
|
+
return Either.right(menuCommand);
|
|
31
|
+
}
|
|
32
|
+
if (args.some((arg) => isHelpFlag(arg))) {
|
|
33
|
+
return Either.right(helpCommand);
|
|
34
|
+
}
|
|
35
|
+
const command = args[0];
|
|
36
|
+
const rest = args.slice(1);
|
|
37
|
+
const unknownCommandError = {
|
|
38
|
+
_tag: "UnknownCommand",
|
|
39
|
+
command: command ?? ""
|
|
40
|
+
};
|
|
41
|
+
return Match.value(command).pipe(Match.when("create", () => parseCreate(rest)), Match.when("init", () => parseCreate(rest)), Match.when("clone", () => parseClone(rest)), Match.when("attach", () => parseAttach(rest)), Match.when("tmux", () => parseAttach(rest)), Match.when("panes", () => parsePanes(rest)), Match.when("terms", () => parsePanes(rest)), Match.when("terminals", () => parsePanes(rest)), Match.when("sessions", () => parseSessions(rest)), Match.when("help", () => Either.right(helpCommand)), Match.when("ps", () => Either.right(statusCommand)), Match.when("status", () => Either.right(statusCommand)), Match.when("down-all", () => Either.right(downAllCommand)), Match.when("stop-all", () => Either.right(downAllCommand)), Match.when("kill-all", () => Either.right(downAllCommand)), Match.when("menu", () => Either.right(menuCommand)), Match.when("ui", () => Either.right(menuCommand)), Match.when("auth", () => parseAuth(rest)), Match.when("state", () => parseState(rest)), Match.orElse(() => Either.left(unknownCommandError)));
|
|
42
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Effect, Either, pipe } from "effect";
|
|
2
|
+
import {} from "@effect-template/lib/core/domain";
|
|
3
|
+
import { parseArgs } from "./parser.js";
|
|
4
|
+
// CHANGE: read and parse CLI arguments from process.argv
|
|
5
|
+
// WHY: keep IO at the boundary and delegate parsing to CORE
|
|
6
|
+
// QUOTE(ТЗ): "Надо написать CLI команду"
|
|
7
|
+
// REF: user-request-2026-01-07
|
|
8
|
+
// SOURCE: n/a
|
|
9
|
+
// FORMAT THEOREM: forall argv: read(argv) -> parse(argv)
|
|
10
|
+
// PURITY: SHELL
|
|
11
|
+
// EFFECT: Effect<Command, ParseError, never>
|
|
12
|
+
// INVARIANT: errors are typed as ParseError
|
|
13
|
+
// COMPLEXITY: O(n) where n = |argv|
|
|
14
|
+
export const readCommand = pipe(Effect.sync(() => process.argv.slice(2)), Effect.map((args) => parseArgs(args)), Effect.flatMap((result) => Either.match(result, {
|
|
15
|
+
onLeft: (error) => Effect.fail(error),
|
|
16
|
+
onRight: (command) => Effect.succeed(command)
|
|
17
|
+
})));
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { Match } from "effect";
|
|
2
|
+
export const usageText = `docker-git menu
|
|
3
|
+
docker-git create --repo-url <url> [options]
|
|
4
|
+
docker-git clone <url> [options]
|
|
5
|
+
docker-git attach [<url>] [options]
|
|
6
|
+
docker-git panes [<url>] [options]
|
|
7
|
+
docker-git sessions [list] [<url>] [options]
|
|
8
|
+
docker-git sessions kill <pid> [<url>] [options]
|
|
9
|
+
docker-git sessions logs <pid> [<url>] [options]
|
|
10
|
+
docker-git ps
|
|
11
|
+
docker-git down-all
|
|
12
|
+
docker-git auth <provider> <action> [options]
|
|
13
|
+
docker-git state <action> [options]
|
|
14
|
+
|
|
15
|
+
Commands:
|
|
16
|
+
menu Interactive menu (default when no args)
|
|
17
|
+
create, init Generate docker development environment
|
|
18
|
+
clone Create + run container and clone repo
|
|
19
|
+
attach, tmux Open tmux workspace for a docker-git project
|
|
20
|
+
panes, terms List tmux panes for a docker-git project
|
|
21
|
+
sessions List/kill/log container terminal processes
|
|
22
|
+
ps, status Show docker compose status for all docker-git projects
|
|
23
|
+
down-all Stop all docker-git containers (docker compose down)
|
|
24
|
+
auth Manage GitHub/Codex auth for docker-git
|
|
25
|
+
state Manage docker-git state directory via git (sync across machines)
|
|
26
|
+
|
|
27
|
+
Options:
|
|
28
|
+
--repo-ref <ref> Git ref/branch (default: main)
|
|
29
|
+
--branch, -b <ref> Alias for --repo-ref
|
|
30
|
+
--target-dir <path> Target dir inside container (create default: /home/dev/app, clone default: /home/dev/<org>/<repo>[/issue-<id>|/pr-<id>])
|
|
31
|
+
--ssh-port <port> Local SSH port (default: 2222)
|
|
32
|
+
--ssh-user <user> SSH user inside container (default: dev)
|
|
33
|
+
--container-name <name> Docker container name (default: dg-<repo>)
|
|
34
|
+
--service-name <name> Compose service name (default: dg-<repo>)
|
|
35
|
+
--volume-name <name> Docker volume name (default: dg-<repo>-home)
|
|
36
|
+
--secrets-root <path> Host root for shared secrets (default: n/a)
|
|
37
|
+
--authorized-keys <path> Host path to authorized_keys (default: <projectsRoot>/authorized_keys)
|
|
38
|
+
--env-global <path> Host path to shared env file (default: <projectsRoot>/.orch/env/global.env)
|
|
39
|
+
--env-project <path> Host path to project env file (default: ./.orch/env/project.env)
|
|
40
|
+
--codex-auth <path> Host path for Codex auth cache (default: <projectsRoot>/.orch/auth/codex)
|
|
41
|
+
--codex-home <path> Container path for Codex auth (default: /home/dev/.codex)
|
|
42
|
+
--out-dir <path> Output directory (default: <projectsRoot>/<org>/<repo>[/issue-<id>|/pr-<id>])
|
|
43
|
+
--project-dir <path> Project directory for attach (default: .)
|
|
44
|
+
--lines <n> Tail last N lines for sessions logs (default: 200)
|
|
45
|
+
--include-default Show default/system processes in sessions list
|
|
46
|
+
--up | --no-up Run docker compose up after init (default: --up)
|
|
47
|
+
--mcp-playwright | --no-mcp-playwright Enable Playwright MCP + Chromium sidecar (default: --no-mcp-playwright)
|
|
48
|
+
--force Overwrite existing files and wipe compose volumes (docker compose down -v)
|
|
49
|
+
--force-env Reset project env defaults only (keep workspace volume/data)
|
|
50
|
+
-h, --help Show this help
|
|
51
|
+
|
|
52
|
+
Container runtime env (set via .orch/env/project.env):
|
|
53
|
+
CODEX_SHARE_AUTH=1|0 Share Codex auth.json across projects (default: 1)
|
|
54
|
+
CODEX_AUTO_UPDATE=1|0 Auto-update Codex CLI on container start (default: 1)
|
|
55
|
+
DOCKER_GIT_ZSH_AUTOSUGGEST=1|0 Enable zsh-autosuggestions (default: 1)
|
|
56
|
+
DOCKER_GIT_ZSH_AUTOSUGGEST_STYLE=... zsh-autosuggestions highlight style (default: fg=8,italic)
|
|
57
|
+
DOCKER_GIT_ZSH_AUTOSUGGEST_STRATEGY=... Suggestion sources (default: history completion)
|
|
58
|
+
MCP_PLAYWRIGHT_ISOLATED=1|0 Isolated browser contexts (recommended for many Codex; default: 1)
|
|
59
|
+
MCP_PLAYWRIGHT_CDP_ENDPOINT=http://... Override CDP endpoint (default: http://dg-<repo>-browser:9223)
|
|
60
|
+
|
|
61
|
+
Auth providers:
|
|
62
|
+
github, gh GitHub CLI auth (tokens saved to env file)
|
|
63
|
+
codex Codex CLI auth (stored under .orch/auth/codex)
|
|
64
|
+
|
|
65
|
+
Auth actions:
|
|
66
|
+
login Run login flow and store credentials
|
|
67
|
+
status Show current auth status
|
|
68
|
+
logout Remove stored credentials
|
|
69
|
+
|
|
70
|
+
Auth options:
|
|
71
|
+
--label <label> Account label (default: default)
|
|
72
|
+
--token <token> GitHub token override (login only)
|
|
73
|
+
--scopes <scopes> GitHub scopes (login only, default: repo,workflow,read:org)
|
|
74
|
+
--env-global <path> Env file path for GitHub tokens (default: <projectsRoot>/.orch/env/global.env)
|
|
75
|
+
--codex-auth <path> Codex auth root path (default: <projectsRoot>/.orch/auth/codex)
|
|
76
|
+
|
|
77
|
+
State actions:
|
|
78
|
+
state path Print current projects root (default: ~/.docker-git; override via DOCKER_GIT_PROJECTS_ROOT)
|
|
79
|
+
state init --repo-url <url> [-b] Init / bind state dir to a git remote (use a private repo)
|
|
80
|
+
state status Show git status for the state dir
|
|
81
|
+
state pull git pull (state dir)
|
|
82
|
+
state commit -m <message> Commit all changes in the state dir
|
|
83
|
+
state sync [-m <message>] Commit (if needed) + fetch/rebase + push (state dir); on conflict pushes a PR branch
|
|
84
|
+
state push git push (state dir)
|
|
85
|
+
|
|
86
|
+
State options:
|
|
87
|
+
--message, -m <message> Commit message for state commit
|
|
88
|
+
`;
|
|
89
|
+
// CHANGE: normalize parse errors into user-facing messages
|
|
90
|
+
// WHY: keep formatting deterministic and centralized
|
|
91
|
+
// QUOTE(ТЗ): "Надо написать CLI команду"
|
|
92
|
+
// REF: user-request-2026-01-07
|
|
93
|
+
// SOURCE: n/a
|
|
94
|
+
// FORMAT THEOREM: forall e: format(e) = s -> deterministic(s)
|
|
95
|
+
// PURITY: CORE
|
|
96
|
+
// EFFECT: Effect<string, never, never>
|
|
97
|
+
// INVARIANT: each ParseError maps to exactly one message
|
|
98
|
+
// COMPLEXITY: O(1)
|
|
99
|
+
export 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);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { NodeContext, NodeRuntime } from "@effect/platform-node";
|
|
2
|
+
import { Effect } from "effect";
|
|
3
|
+
import { program } from "./program.js";
|
|
4
|
+
// CHANGE: run docker-git CLI through the Node runtime
|
|
5
|
+
// WHY: ensure platform services (FS, Path, Command) are available in app CLI
|
|
6
|
+
// QUOTE(ТЗ): "CLI (отображение, фронт) это app"
|
|
7
|
+
// REF: user-request-2026-01-28-cli-move
|
|
8
|
+
// SOURCE: n/a
|
|
9
|
+
// FORMAT THEOREM: forall env: runMain(program, env) -> exit
|
|
10
|
+
// PURITY: SHELL
|
|
11
|
+
// EFFECT: Effect<void, unknown, NodeContext>
|
|
12
|
+
// INVARIANT: program runs with NodeContext.layer
|
|
13
|
+
// COMPLEXITY: O(n)
|
|
14
|
+
const main = Effect.provide(program, NodeContext.layer);
|
|
15
|
+
NodeRuntime.runMain(main);
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import {} from "@effect-template/lib/core/domain";
|
|
2
|
+
import { readProjectConfig } from "@effect-template/lib/shell/config";
|
|
3
|
+
import { runDockerComposeDown, runDockerComposeLogs, runDockerComposePs } from "@effect-template/lib/shell/docker";
|
|
4
|
+
import { renderError } from "@effect-template/lib/usecases/errors";
|
|
5
|
+
import { downAllDockerGitProjects, listProjectItems, listRunningProjectItems } from "@effect-template/lib/usecases/projects";
|
|
6
|
+
import { runDockerComposeUpWithPortCheck } from "@effect-template/lib/usecases/projects-up";
|
|
7
|
+
import { Effect, Match, pipe } from "effect";
|
|
8
|
+
import { startCreateView } from "./menu-create.js";
|
|
9
|
+
import { loadSelectView } from "./menu-select.js";
|
|
10
|
+
import { resumeTui, suspendTui } from "./menu-shared.js";
|
|
11
|
+
import {} from "./menu-types.js";
|
|
12
|
+
// CHANGE: keep menu actions and input parsing in a dedicated module
|
|
13
|
+
// WHY: reduce cognitive complexity in the TUI entry
|
|
14
|
+
// QUOTE(ТЗ): "TUI? Красивый, удобный"
|
|
15
|
+
// REF: user-request-2026-02-01-tui
|
|
16
|
+
// SOURCE: n/a
|
|
17
|
+
// FORMAT THEOREM: forall a: action(a) -> effect(a)
|
|
18
|
+
// PURITY: SHELL
|
|
19
|
+
// EFFECT: Effect<void, AppError, MenuEnv>
|
|
20
|
+
// INVARIANT: menu selection runs exactly one action
|
|
21
|
+
// COMPLEXITY: O(1) per keypress
|
|
22
|
+
const continueOutcome = (state) => ({
|
|
23
|
+
_tag: "Continue",
|
|
24
|
+
state
|
|
25
|
+
});
|
|
26
|
+
const quitOutcome = { _tag: "Quit" };
|
|
27
|
+
const actionLabel = (action) => Match.value(action).pipe(Match.when({ _tag: "Up" }, () => "docker compose up"), Match.when({ _tag: "Status" }, () => "docker compose ps"), Match.when({ _tag: "Logs" }, () => "docker compose logs"), Match.when({ _tag: "Down" }, () => "docker compose down"), Match.when({ _tag: "DownAll" }, () => "docker compose down (all projects)"), Match.orElse(() => "action"));
|
|
28
|
+
const runWithSuspendedTui = (effect, context, label) => {
|
|
29
|
+
context.runner.runEffect(pipe(Effect.sync(() => {
|
|
30
|
+
context.setMessage(`${label}...`);
|
|
31
|
+
suspendTui();
|
|
32
|
+
}), Effect.zipRight(effect), Effect.tap(() => Effect.sync(() => {
|
|
33
|
+
context.setMessage(`${label} finished.`);
|
|
34
|
+
})), Effect.ensuring(Effect.sync(() => {
|
|
35
|
+
resumeTui();
|
|
36
|
+
})), Effect.asVoid));
|
|
37
|
+
};
|
|
38
|
+
const requireActiveProject = (context) => {
|
|
39
|
+
if (context.state.activeDir) {
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
context.setMessage("No active project. Use Create or paste a repo URL to set one before running this action.");
|
|
43
|
+
return false;
|
|
44
|
+
};
|
|
45
|
+
const handleMissingConfig = (state, setMessage, error) => pipe(Effect.sync(() => {
|
|
46
|
+
setMessage(renderError(error));
|
|
47
|
+
}), Effect.as(continueOutcome(state)));
|
|
48
|
+
const withProjectConfig = (state, setMessage, f) => pipe(readProjectConfig(state.activeDir ?? state.cwd), Effect.matchEffect({
|
|
49
|
+
onFailure: (error) => error._tag === "ConfigNotFoundError" || error._tag === "ConfigDecodeError"
|
|
50
|
+
? handleMissingConfig(state, setMessage, error)
|
|
51
|
+
: Effect.fail(error),
|
|
52
|
+
onSuccess: (config) => pipe(f(config), Effect.as(continueOutcome(state)))
|
|
53
|
+
}));
|
|
54
|
+
const handleMenuAction = (state, setMessage, action) => Match.value(action).pipe(Match.when({ _tag: "Quit" }, () => Effect.succeed(quitOutcome)), Match.when({ _tag: "Create" }, () => Effect.succeed(continueOutcome(state))), Match.when({ _tag: "Select" }, () => Effect.succeed(continueOutcome(state))), Match.when({ _tag: "Info" }, () => Effect.succeed(continueOutcome(state))), Match.when({ _tag: "Delete" }, () => Effect.succeed(continueOutcome(state))), Match.when({ _tag: "Up" }, () => withProjectConfig(state, setMessage, () => runDockerComposeUpWithPortCheck(state.activeDir ?? state.cwd).pipe(Effect.asVoid))), Match.when({ _tag: "Status" }, () => withProjectConfig(state, setMessage, () => runDockerComposePs(state.activeDir ?? state.cwd))), Match.when({ _tag: "Logs" }, () => withProjectConfig(state, setMessage, () => runDockerComposeLogs(state.activeDir ?? state.cwd))), Match.when({ _tag: "Down" }, () => withProjectConfig(state, setMessage, () => runDockerComposeDown(state.activeDir ?? state.cwd))), Match.when({ _tag: "DownAll" }, () => pipe(downAllDockerGitProjects, Effect.as(continueOutcome(state)))), Match.exhaustive);
|
|
55
|
+
const runCreateAction = (context) => {
|
|
56
|
+
startCreateView(context.setView, context.setMessage);
|
|
57
|
+
};
|
|
58
|
+
const runSelectAction = (context) => {
|
|
59
|
+
context.setMessage(null);
|
|
60
|
+
context.runner.runEffect(loadSelectView(listProjectItems, "Connect", context));
|
|
61
|
+
};
|
|
62
|
+
const runDownAllAction = (context) => {
|
|
63
|
+
context.setMessage(null);
|
|
64
|
+
runWithSuspendedTui(downAllDockerGitProjects, context, "Stopping all docker-git containers");
|
|
65
|
+
};
|
|
66
|
+
const runDownAction = (context, action) => {
|
|
67
|
+
context.setMessage(null);
|
|
68
|
+
if (context.state.activeDir === null) {
|
|
69
|
+
context.runner.runEffect(loadSelectView(listRunningProjectItems, "Down", context));
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
runComposeAction(action, context);
|
|
73
|
+
};
|
|
74
|
+
const runInfoAction = (context) => {
|
|
75
|
+
context.setMessage(null);
|
|
76
|
+
context.runner.runEffect(loadSelectView(listProjectItems, "Info", context));
|
|
77
|
+
};
|
|
78
|
+
const runDeleteAction = (context) => {
|
|
79
|
+
context.setMessage(null);
|
|
80
|
+
context.runner.runEffect(loadSelectView(listProjectItems, "Delete", context));
|
|
81
|
+
};
|
|
82
|
+
const runComposeAction = (action, context) => {
|
|
83
|
+
if (!requireActiveProject(context)) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const effect = pipe(handleMenuAction(context.state, context.setMessage, action), Effect.asVoid);
|
|
87
|
+
runWithSuspendedTui(effect, context, actionLabel(action));
|
|
88
|
+
};
|
|
89
|
+
const runQuitAction = (context, action) => {
|
|
90
|
+
context.runner.runEffect(pipe(handleMenuAction(context.state, context.setMessage, action), Effect.asVoid));
|
|
91
|
+
context.exit();
|
|
92
|
+
};
|
|
93
|
+
export const handleMenuActionSelection = (action, context) => {
|
|
94
|
+
Match.value(action).pipe(Match.when({ _tag: "Create" }, () => {
|
|
95
|
+
runCreateAction(context);
|
|
96
|
+
}), Match.when({ _tag: "Select" }, () => {
|
|
97
|
+
runSelectAction(context);
|
|
98
|
+
}), Match.when({ _tag: "Info" }, () => {
|
|
99
|
+
runInfoAction(context);
|
|
100
|
+
}), Match.when({ _tag: "Delete" }, () => {
|
|
101
|
+
runDeleteAction(context);
|
|
102
|
+
}), Match.when({ _tag: "Up" }, (selected) => {
|
|
103
|
+
runComposeAction(selected, context);
|
|
104
|
+
}), Match.when({ _tag: "Status" }, (selected) => {
|
|
105
|
+
runComposeAction(selected, context);
|
|
106
|
+
}), Match.when({ _tag: "Logs" }, (selected) => {
|
|
107
|
+
runComposeAction(selected, context);
|
|
108
|
+
}), Match.when({ _tag: "Down" }, (selected) => {
|
|
109
|
+
runDownAction(context, selected);
|
|
110
|
+
}), Match.when({ _tag: "DownAll" }, () => {
|
|
111
|
+
runDownAllAction(context);
|
|
112
|
+
}), Match.when({ _tag: "Quit" }, (selected) => {
|
|
113
|
+
runQuitAction(context, selected);
|
|
114
|
+
}), Match.exhaustive);
|
|
115
|
+
};
|