@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
@@ -0,0 +1,75 @@
1
+ import { listProjects, readCloneRequest, runDockerGitClone } from "@effect-template/lib"
2
+ import { Console, Effect, Match, pipe } from "effect"
3
+
4
+ /**
5
+ * Compose the CLI program as a single effect.
6
+ *
7
+ * @returns Effect that either runs docker-git clone or prints usage.
8
+ *
9
+ * @pure false - uses Console output and spawns commands when cloning
10
+ * @effect Console, CommandExecutor, Path
11
+ * @invariant forall args in Argv: clone(args) -> docker_git_invoked(args)
12
+ * @precondition true
13
+ * @postcondition clone(args) -> docker_git_invoked(args); otherwise usage printed
14
+ * @complexity O(build + clone)
15
+ * @throws Never - all errors are typed in the Effect error channel
16
+ */
17
+ // CHANGE: replace greeting demo with deterministic usage text
18
+ // WHY: greeting was scaffolding noise and should not ship in docker-git tooling
19
+ // QUOTE(ТЗ): "Можешь удалить использование greting ...? Это старый мусор который остался"
20
+ // REF: user-request-2026-02-06-remove-greeting
21
+ // SOURCE: n/a
22
+ // FORMAT THEOREM: usageText is constant -> deterministic(help)
23
+ // PURITY: CORE
24
+ // EFFECT: n/a
25
+ // INVARIANT: usageText does not depend on argv/env
26
+ // COMPLEXITY: O(1)
27
+ const usageText = [
28
+ "Usage:",
29
+ " pnpm docker-git",
30
+ " pnpm clone <repo-url> [ref]",
31
+ " pnpm list",
32
+ "",
33
+ "Notes:",
34
+ " - docker-git is the interactive TUI.",
35
+ " - clone builds + runs docker-git clone for you."
36
+ ].join("\n")
37
+
38
+ // PURITY: SHELL
39
+ // EFFECT: Effect<void, never, Console>
40
+ const runHelp = Console.log(usageText)
41
+
42
+ // CHANGE: route between clone runner and help based on CLI context
43
+ // WHY: allow pnpm run clone <url> while keeping a single entrypoint
44
+ // QUOTE(ТЗ): "pnpm run clone <url>"
45
+ // REF: user-request-2026-01-27
46
+ // SOURCE: n/a
47
+ // FORMAT THEOREM: forall argv: clone(argv) -> docker_git_invoked(argv)
48
+ // PURITY: SHELL
49
+ // EFFECT: Effect<void, Error, Console | CommandExecutor | Path>
50
+ // INVARIANT: help is printed when clone is not requested
51
+ // COMPLEXITY: O(build + clone)
52
+ const runDockerGit = pipe(
53
+ readCloneRequest,
54
+ Effect.flatMap((request) =>
55
+ Match.value(request).pipe(
56
+ Match.when({ _tag: "Clone" }, ({ args }) => runDockerGitClone(args)),
57
+ Match.when({ _tag: "None" }, () => runHelp),
58
+ Match.exhaustive
59
+ )
60
+ )
61
+ )
62
+
63
+ const readListFlag = Effect.sync(() => {
64
+ const command = process.argv.slice(2)[0] ?? ""
65
+ return command === "list" || command === "ls"
66
+ })
67
+
68
+ export const program = Effect.gen(function*(_) {
69
+ const isList = yield* _(readListFlag)
70
+ if (isList) {
71
+ yield* _(listProjects)
72
+ return
73
+ }
74
+ yield* _(runDockerGit)
75
+ })
@@ -0,0 +1,29 @@
1
+ import * as Terminal from "@effect/platform/Terminal"
2
+ import { Effect } from "effect"
3
+
4
+ import { InputCancelledError, InputReadError } from "@effect-template/lib/shell/errors"
5
+
6
+ const normalizeMessage = (error: Error): string => error.message
7
+
8
+ const toReadError = (error: Error): InputReadError => new InputReadError({ message: normalizeMessage(error) })
9
+
10
+ const mapReadLineError = (_error: Terminal.QuitException): InputCancelledError => new InputCancelledError({})
11
+
12
+ // CHANGE: prompt for a single line of user input
13
+ // WHY: provide an interactive CLI without raw terminal mode issues
14
+ // QUOTE(ТЗ): "Хочу что бы открылось менюшка"
15
+ // REF: user-request-2026-01-07
16
+ // SOURCE: n/a
17
+ // FORMAT THEOREM: forall p: prompt(p) -> line(p)
18
+ // PURITY: SHELL
19
+ // EFFECT: Effect<string, InputCancelledError | InputReadError, never>
20
+ // INVARIANT: restores raw mode if it was enabled before prompting
21
+ // COMPLEXITY: O(1)
22
+ export const promptLine = (
23
+ prompt: string
24
+ ): Effect.Effect<string, InputCancelledError | InputReadError, Terminal.Terminal> =>
25
+ Effect.gen(function*(_) {
26
+ const terminal = yield* _(Terminal.Terminal)
27
+ yield* _(terminal.display(prompt).pipe(Effect.mapError(toReadError)))
28
+ return yield* _(terminal.readLine.pipe(Effect.mapError(mapReadLineError)))
29
+ })
@@ -0,0 +1,22 @@
1
+ import { Either } from "effect"
2
+
3
+ import { type AttachCommand, type ParseError } from "@effect-template/lib/core/domain"
4
+
5
+ import { parseProjectDirArgs } from "./parser-shared.js"
6
+
7
+ // CHANGE: parse attach command into a project selection
8
+ // WHY: allow "docker-git attach" to open a tmux workspace
9
+ // QUOTE(ТЗ): "окей Давай подключим tmux"
10
+ // REF: user-request-2026-02-02-tmux
11
+ // SOURCE: n/a
12
+ // FORMAT THEOREM: forall argv: parseAttach(argv) = cmd -> deterministic(cmd)
13
+ // PURITY: CORE
14
+ // EFFECT: Effect<AttachCommand, ParseError, never>
15
+ // INVARIANT: projectDir is never empty
16
+ // COMPLEXITY: O(n) where n = |argv|
17
+ export const parseAttach = (args: ReadonlyArray<string>): Either.Either<AttachCommand, ParseError> => {
18
+ return Either.map(parseProjectDirArgs(args), ({ projectDir }) => ({
19
+ _tag: "Attach",
20
+ projectDir
21
+ }))
22
+ }
@@ -0,0 +1,124 @@
1
+ import { Either, Match } from "effect"
2
+
3
+ import type { RawOptions } from "@effect-template/lib/core/command-options"
4
+ import {
5
+ type AuthCommand,
6
+ type Command,
7
+ defaultTemplateConfig,
8
+ type ParseError
9
+ } from "@effect-template/lib/core/domain"
10
+
11
+ import { parseRawOptions } from "./parser-options.js"
12
+
13
+ type AuthOptions = {
14
+ readonly envGlobalPath: string
15
+ readonly codexAuthPath: string
16
+ readonly label: string | null
17
+ readonly token: string | null
18
+ readonly scopes: string | null
19
+ }
20
+
21
+ const missingArgument = (name: string): ParseError => ({
22
+ _tag: "MissingRequiredOption",
23
+ option: name
24
+ })
25
+
26
+ const invalidArgument = (name: string, reason: string): ParseError => ({
27
+ _tag: "InvalidOption",
28
+ option: name,
29
+ reason
30
+ })
31
+
32
+ const normalizeLabel = (value: string | undefined): string | null => {
33
+ const trimmed = value?.trim() ?? ""
34
+ return trimmed.length === 0 ? null : trimmed
35
+ }
36
+
37
+ const resolveAuthOptions = (raw: RawOptions): AuthOptions => ({
38
+ envGlobalPath: raw.envGlobalPath ?? defaultTemplateConfig.envGlobalPath,
39
+ codexAuthPath: raw.codexAuthPath ?? defaultTemplateConfig.codexAuthPath,
40
+ label: normalizeLabel(raw.label),
41
+ token: normalizeLabel(raw.token),
42
+ scopes: normalizeLabel(raw.scopes)
43
+ })
44
+
45
+ const buildGithubCommand = (action: string, options: AuthOptions): Either.Either<AuthCommand, ParseError> =>
46
+ Match.value(action).pipe(
47
+ Match.when("login", () =>
48
+ Either.right<AuthCommand>({
49
+ _tag: "AuthGithubLogin",
50
+ label: options.label,
51
+ token: options.token,
52
+ scopes: options.scopes,
53
+ envGlobalPath: options.envGlobalPath
54
+ })),
55
+ Match.when("status", () =>
56
+ Either.right<AuthCommand>({
57
+ _tag: "AuthGithubStatus",
58
+ envGlobalPath: options.envGlobalPath
59
+ })),
60
+ Match.when("logout", () =>
61
+ Either.right<AuthCommand>({
62
+ _tag: "AuthGithubLogout",
63
+ label: options.label,
64
+ envGlobalPath: options.envGlobalPath
65
+ })),
66
+ Match.orElse(() => Either.left(invalidArgument("auth action", `unknown action '${action}'`)))
67
+ )
68
+
69
+ const buildCodexCommand = (action: string, options: AuthOptions): Either.Either<AuthCommand, ParseError> =>
70
+ Match.value(action).pipe(
71
+ Match.when("login", () =>
72
+ Either.right<AuthCommand>({
73
+ _tag: "AuthCodexLogin",
74
+ label: options.label,
75
+ codexAuthPath: options.codexAuthPath
76
+ })),
77
+ Match.when("status", () =>
78
+ Either.right<AuthCommand>({
79
+ _tag: "AuthCodexStatus",
80
+ label: options.label,
81
+ codexAuthPath: options.codexAuthPath
82
+ })),
83
+ Match.when("logout", () =>
84
+ Either.right<AuthCommand>({
85
+ _tag: "AuthCodexLogout",
86
+ label: options.label,
87
+ codexAuthPath: options.codexAuthPath
88
+ })),
89
+ Match.orElse(() => Either.left(invalidArgument("auth action", `unknown action '${action}'`)))
90
+ )
91
+
92
+ const buildAuthCommand = (
93
+ provider: string,
94
+ action: string,
95
+ options: AuthOptions
96
+ ): Either.Either<AuthCommand, ParseError> =>
97
+ Match.value(provider).pipe(
98
+ Match.when("github", () => buildGithubCommand(action, options)),
99
+ Match.when("gh", () => buildGithubCommand(action, options)),
100
+ Match.when("codex", () => buildCodexCommand(action, options)),
101
+ Match.orElse(() => Either.left(invalidArgument("auth provider", `unknown provider '${provider}'`)))
102
+ )
103
+
104
+ // CHANGE: parse docker-git auth subcommands
105
+ // WHY: keep auth flows in the same typed CLI parser
106
+ // QUOTE(ТЗ): "система авторизации"
107
+ // REF: user-request-2026-01-28-auth
108
+ // SOURCE: n/a
109
+ // FORMAT THEOREM: forall argv: parseAuth(argv) = cmd | error
110
+ // PURITY: CORE
111
+ // EFFECT: Effect<Command, ParseError, never>
112
+ // INVARIANT: no IO or side effects
113
+ // COMPLEXITY: O(n) where n = |argv|
114
+ export const parseAuth = (args: ReadonlyArray<string>): Either.Either<Command, ParseError> => {
115
+ if (args.length < 2) {
116
+ return Either.left(missingArgument(args.length === 0 ? "auth provider" : "auth action"))
117
+ }
118
+
119
+ const provider = args[0] ?? ""
120
+ const action = args[1] ?? ""
121
+ const rest = args.slice(2)
122
+
123
+ return Either.flatMap(parseRawOptions(rest), (raw) => buildAuthCommand(provider, action, resolveAuthOptions(raw)))
124
+ }
@@ -0,0 +1,55 @@
1
+ import { Either } from "effect"
2
+
3
+ import { buildCreateCommand, nonEmpty } from "@effect-template/lib/core/command-builders"
4
+ import type { RawOptions } from "@effect-template/lib/core/command-options"
5
+ import {
6
+ type Command,
7
+ defaultTemplateConfig,
8
+ type ParseError,
9
+ resolveRepoInput
10
+ } from "@effect-template/lib/core/domain"
11
+
12
+ import { parseRawOptions } from "./parser-options.js"
13
+ import { resolveWorkspaceRepoPath, splitPositionalRepo } from "./parser-shared.js"
14
+
15
+ const applyCloneDefaults = (
16
+ raw: RawOptions,
17
+ rawRepoUrl: string,
18
+ resolvedRepo: ReturnType<typeof resolveRepoInput>
19
+ ): RawOptions => {
20
+ const repoPath = resolveWorkspaceRepoPath(resolvedRepo)
21
+ const sshUser = raw.sshUser?.trim() ?? defaultTemplateConfig.sshUser
22
+ const homeDir = `/home/${sshUser}`
23
+ return {
24
+ ...raw,
25
+ repoUrl: rawRepoUrl,
26
+ outDir: raw.outDir ?? `.docker-git/${repoPath}`,
27
+ targetDir: raw.targetDir ?? `${homeDir}/${repoPath}`
28
+ }
29
+ }
30
+
31
+ // CHANGE: parse clone command with positional repo url
32
+ // WHY: allow "docker-git clone <url>" to build + run a container
33
+ // QUOTE(ТЗ): "docker-git clone url"
34
+ // REF: user-request-2026-01-27
35
+ // SOURCE: n/a
36
+ // FORMAT THEOREM: forall argv: parseClone(argv) = cmd -> deterministic(cmd)
37
+ // PURITY: CORE
38
+ // EFFECT: Effect<Command, ParseError, never>
39
+ // INVARIANT: first positional arg is treated as repo url
40
+ // COMPLEXITY: O(n) where n = |argv|
41
+ export const parseClone = (args: ReadonlyArray<string>): Either.Either<Command, ParseError> => {
42
+ const { positionalRepoUrl, restArgs } = splitPositionalRepo(args)
43
+
44
+ return Either.gen(function*(_) {
45
+ const raw = yield* _(parseRawOptions(restArgs))
46
+ const rawRepoUrl = yield* _(nonEmpty("--repo-url", raw.repoUrl ?? positionalRepoUrl))
47
+ const resolvedRepo = resolveRepoInput(rawRepoUrl)
48
+ const withDefaults = applyCloneDefaults(raw, rawRepoUrl, resolvedRepo)
49
+ const withRef = resolvedRepo.repoRef !== undefined && raw.repoRef === undefined
50
+ ? { ...withDefaults, repoRef: resolvedRepo.repoRef }
51
+ : withDefaults
52
+ const create = yield* _(buildCreateCommand(withRef))
53
+ return { ...create, waitForClone: true }
54
+ })
55
+ }
@@ -0,0 +1,3 @@
1
+ export { buildCreateCommand, nonEmpty } from "@effect-template/lib/core/command-builders"
2
+ export type { RawOptions } from "@effect-template/lib/core/command-options"
3
+ export type { CreateCommand, ParseError } from "@effect-template/lib/core/domain"
@@ -0,0 +1,152 @@
1
+ import { Either } from "effect"
2
+
3
+ import type { RawOptions } from "@effect-template/lib/core/command-options"
4
+ import type { ParseError } from "@effect-template/lib/core/domain"
5
+
6
+ interface ValueOptionSpec {
7
+ readonly flag: string
8
+ readonly key:
9
+ | "repoUrl"
10
+ | "repoRef"
11
+ | "targetDir"
12
+ | "sshPort"
13
+ | "sshUser"
14
+ | "containerName"
15
+ | "serviceName"
16
+ | "volumeName"
17
+ | "secretsRoot"
18
+ | "authorizedKeysPath"
19
+ | "envGlobalPath"
20
+ | "envProjectPath"
21
+ | "codexAuthPath"
22
+ | "codexHome"
23
+ | "label"
24
+ | "token"
25
+ | "scopes"
26
+ | "message"
27
+ | "outDir"
28
+ | "projectDir"
29
+ | "lines"
30
+ }
31
+
32
+ const valueOptionSpecs: ReadonlyArray<ValueOptionSpec> = [
33
+ { flag: "--repo-url", key: "repoUrl" },
34
+ { flag: "--repo-ref", key: "repoRef" },
35
+ { flag: "--branch", key: "repoRef" },
36
+ { flag: "-b", key: "repoRef" },
37
+ { flag: "--target-dir", key: "targetDir" },
38
+ { flag: "--ssh-port", key: "sshPort" },
39
+ { flag: "--ssh-user", key: "sshUser" },
40
+ { flag: "--container-name", key: "containerName" },
41
+ { flag: "--service-name", key: "serviceName" },
42
+ { flag: "--volume-name", key: "volumeName" },
43
+ { flag: "--secrets-root", key: "secretsRoot" },
44
+ { flag: "--authorized-keys", key: "authorizedKeysPath" },
45
+ { flag: "--env-global", key: "envGlobalPath" },
46
+ { flag: "--env-project", key: "envProjectPath" },
47
+ { flag: "--codex-auth", key: "codexAuthPath" },
48
+ { flag: "--codex-home", key: "codexHome" },
49
+ { flag: "--label", key: "label" },
50
+ { flag: "--token", key: "token" },
51
+ { flag: "--scopes", key: "scopes" },
52
+ { flag: "--message", key: "message" },
53
+ { flag: "-m", key: "message" },
54
+ { flag: "--out-dir", key: "outDir" },
55
+ { flag: "--project-dir", key: "projectDir" },
56
+ { flag: "--lines", key: "lines" }
57
+ ]
58
+
59
+ const valueOptionSpecByFlag: ReadonlyMap<string, ValueOptionSpec> = new Map(
60
+ valueOptionSpecs.map((spec) => [spec.flag, spec])
61
+ )
62
+
63
+ type ValueKey = ValueOptionSpec["key"]
64
+
65
+ const booleanFlagUpdaters: Readonly<Record<string, (raw: RawOptions) => RawOptions>> = {
66
+ "--up": (raw) => ({ ...raw, up: true }),
67
+ "--no-up": (raw) => ({ ...raw, up: false }),
68
+ "--force": (raw) => ({ ...raw, force: true }),
69
+ "--force-env": (raw) => ({ ...raw, forceEnv: true }),
70
+ "--mcp-playwright": (raw) => ({ ...raw, enableMcpPlaywright: true }),
71
+ "--no-mcp-playwright": (raw) => ({ ...raw, enableMcpPlaywright: false }),
72
+ "--web": (raw) => ({ ...raw, authWeb: true }),
73
+ "--include-default": (raw) => ({ ...raw, includeDefault: true })
74
+ }
75
+
76
+ const valueFlagUpdaters: { readonly [K in ValueKey]: (raw: RawOptions, value: string) => RawOptions } = {
77
+ repoUrl: (raw, value) => ({ ...raw, repoUrl: value }),
78
+ repoRef: (raw, value) => ({ ...raw, repoRef: value }),
79
+ targetDir: (raw, value) => ({ ...raw, targetDir: value }),
80
+ sshPort: (raw, value) => ({ ...raw, sshPort: value }),
81
+ sshUser: (raw, value) => ({ ...raw, sshUser: value }),
82
+ containerName: (raw, value) => ({ ...raw, containerName: value }),
83
+ serviceName: (raw, value) => ({ ...raw, serviceName: value }),
84
+ volumeName: (raw, value) => ({ ...raw, volumeName: value }),
85
+ secretsRoot: (raw, value) => ({ ...raw, secretsRoot: value }),
86
+ authorizedKeysPath: (raw, value) => ({ ...raw, authorizedKeysPath: value }),
87
+ envGlobalPath: (raw, value) => ({ ...raw, envGlobalPath: value }),
88
+ envProjectPath: (raw, value) => ({ ...raw, envProjectPath: value }),
89
+ codexAuthPath: (raw, value) => ({ ...raw, codexAuthPath: value }),
90
+ codexHome: (raw, value) => ({ ...raw, codexHome: value }),
91
+ label: (raw, value) => ({ ...raw, label: value }),
92
+ token: (raw, value) => ({ ...raw, token: value }),
93
+ scopes: (raw, value) => ({ ...raw, scopes: value }),
94
+ message: (raw, value) => ({ ...raw, message: value }),
95
+ outDir: (raw, value) => ({ ...raw, outDir: value }),
96
+ projectDir: (raw, value) => ({ ...raw, projectDir: value }),
97
+ lines: (raw, value) => ({ ...raw, lines: value })
98
+ }
99
+
100
+ export const applyCommandBooleanFlag = (raw: RawOptions, token: string): RawOptions | null => {
101
+ const updater = booleanFlagUpdaters[token]
102
+ return updater ? updater(raw) : null
103
+ }
104
+
105
+ export const applyCommandValueFlag = (
106
+ raw: RawOptions,
107
+ token: string,
108
+ value: string
109
+ ): Either.Either<RawOptions, ParseError> => {
110
+ const valueSpec = valueOptionSpecByFlag.get(token)
111
+ if (valueSpec === undefined) {
112
+ return Either.left({ _tag: "UnknownOption", option: token })
113
+ }
114
+
115
+ const update = valueFlagUpdaters[valueSpec.key]
116
+ return Either.right(update(raw, value))
117
+ }
118
+
119
+ export const parseRawOptions = (args: ReadonlyArray<string>): Either.Either<RawOptions, ParseError> => {
120
+ let index = 0
121
+ let raw: RawOptions = {}
122
+
123
+ while (index < args.length) {
124
+ const token = args[index] ?? ""
125
+ const booleanApplied = applyCommandBooleanFlag(raw, token)
126
+ if (booleanApplied !== null) {
127
+ raw = booleanApplied
128
+ index += 1
129
+ continue
130
+ }
131
+
132
+ if (!token.startsWith("-")) {
133
+ return Either.left({ _tag: "UnexpectedArgument", value: token })
134
+ }
135
+
136
+ const value = args[index + 1]
137
+ if (value === undefined) {
138
+ return Either.left({ _tag: "MissingOptionValue", option: token })
139
+ }
140
+
141
+ const nextRaw = applyCommandValueFlag(raw, token, value)
142
+ if (Either.isLeft(nextRaw)) {
143
+ return Either.left(nextRaw.left)
144
+ }
145
+ raw = nextRaw.right
146
+ index += 2
147
+ }
148
+
149
+ return Either.right(raw)
150
+ }
151
+
152
+ export { type RawOptions } from "@effect-template/lib/core/command-options"
@@ -0,0 +1,22 @@
1
+ import { Either } from "effect"
2
+
3
+ import { type PanesCommand, type ParseError } from "@effect-template/lib/core/domain"
4
+
5
+ import { parseProjectDirArgs } from "./parser-shared.js"
6
+
7
+ // CHANGE: parse panes command into a project selection
8
+ // WHY: allow listing tmux panes without attaching
9
+ // QUOTE(ТЗ): "покажи команду ... отобразит терминалы"
10
+ // REF: user-request-2026-02-02-panes
11
+ // SOURCE: n/a
12
+ // FORMAT THEOREM: forall argv: parsePanes(argv) = cmd -> deterministic(cmd)
13
+ // PURITY: CORE
14
+ // EFFECT: Effect<PanesCommand, ParseError, never>
15
+ // INVARIANT: projectDir is never empty
16
+ // COMPLEXITY: O(n) where n = |argv|
17
+ export const parsePanes = (args: ReadonlyArray<string>): Either.Either<PanesCommand, ParseError> => {
18
+ return Either.map(parseProjectDirArgs(args), ({ projectDir }) => ({
19
+ _tag: "Panes",
20
+ projectDir
21
+ }))
22
+ }
@@ -0,0 +1,101 @@
1
+ import { Either, Match } from "effect"
2
+
3
+ import { type ParseError, type SessionsCommand } from "@effect-template/lib/core/domain"
4
+
5
+ import { parseProjectDirWithOptions } from "./parser-shared.js"
6
+
7
+ const defaultLines = 200
8
+
9
+ const parsePositiveInt = (
10
+ option: string,
11
+ raw: string
12
+ ): Either.Either<number, ParseError> => {
13
+ const value = Number.parseInt(raw, 10)
14
+ if (!Number.isFinite(value) || value <= 0) {
15
+ const error: ParseError = {
16
+ _tag: "InvalidOption",
17
+ option,
18
+ reason: "expected positive integer"
19
+ }
20
+ return Either.left(error)
21
+ }
22
+ return Either.right(value)
23
+ }
24
+
25
+ const parseList = (args: ReadonlyArray<string>): Either.Either<SessionsCommand, ParseError> =>
26
+ Either.map(parseProjectDirWithOptions(args), ({ projectDir, raw }) => ({
27
+ _tag: "SessionsList",
28
+ projectDir,
29
+ includeDefault: raw.includeDefault === true
30
+ }))
31
+
32
+ const parsePidContext = (
33
+ args: ReadonlyArray<string>
34
+ ): Either.Either<
35
+ { readonly pid: number; readonly projectDir: string; readonly raw: { readonly lines?: string } },
36
+ ParseError
37
+ > =>
38
+ Either.gen(function*(_) {
39
+ const pidRaw = args[0]
40
+ if (!pidRaw) {
41
+ const error: ParseError = { _tag: "MissingRequiredOption", option: "pid" }
42
+ return yield* _(Either.left(error))
43
+ }
44
+ const pid = yield* _(parsePositiveInt("pid", pidRaw))
45
+ const { projectDir, raw } = yield* _(parseProjectDirWithOptions(args.slice(1)))
46
+ return { pid, projectDir, raw }
47
+ })
48
+
49
+ const parseKill = (args: ReadonlyArray<string>): Either.Either<SessionsCommand, ParseError> =>
50
+ Either.map(parsePidContext(args), ({ pid, projectDir }) => ({
51
+ _tag: "SessionsKill",
52
+ projectDir,
53
+ pid
54
+ }))
55
+
56
+ const parseLogs = (args: ReadonlyArray<string>): Either.Either<SessionsCommand, ParseError> =>
57
+ Either.gen(function*(_) {
58
+ const { pid, projectDir, raw } = yield* _(parsePidContext(args))
59
+ const lines = raw.lines ? yield* _(parsePositiveInt("--lines", raw.lines)) : defaultLines
60
+ return { _tag: "SessionsLogs", projectDir, pid, lines }
61
+ })
62
+
63
+ // CHANGE: parse sessions command into list/kill/logs actions
64
+ // WHY: surface container terminal sessions and background processes from CLI
65
+ // QUOTE(ТЗ): "CLI команду которая из докера вернёт запущенные терминал сессии"
66
+ // REF: user-request-2026-02-04-terminal-sessions
67
+ // SOURCE: n/a
68
+ // FORMAT THEOREM: forall argv: parseSessions(argv) = cmd -> deterministic(cmd)
69
+ // PURITY: CORE
70
+ // EFFECT: Effect<SessionsCommand, ParseError, never>
71
+ // INVARIANT: pid/lines must be positive integers
72
+ // COMPLEXITY: O(n) where n = |argv|
73
+ export const parseSessions = (
74
+ args: ReadonlyArray<string>
75
+ ): Either.Either<SessionsCommand, ParseError> => {
76
+ if (args.length === 0) {
77
+ return parseList(args)
78
+ }
79
+
80
+ const first = args[0] ?? ""
81
+ if (first.startsWith("-")) {
82
+ return parseList(args)
83
+ }
84
+
85
+ const rest = args.slice(1)
86
+ return Match.value(first).pipe(
87
+ Match.when("list", () => parseList(rest)),
88
+ Match.when("kill", () => parseKill(rest)),
89
+ Match.when("stop", () => parseKill(rest)),
90
+ Match.when("logs", () => parseLogs(rest)),
91
+ Match.when("log", () => parseLogs(rest)),
92
+ Match.orElse(() => {
93
+ const error: ParseError = {
94
+ _tag: "InvalidOption",
95
+ option: "sessions",
96
+ reason: `unknown action ${first}`
97
+ }
98
+ return Either.left(error)
99
+ })
100
+ )
101
+ }
@@ -0,0 +1,51 @@
1
+ import { Either } from "effect"
2
+
3
+ import { deriveRepoPathParts, type ParseError, resolveRepoInput } from "@effect-template/lib/core/domain"
4
+
5
+ import { parseRawOptions, type RawOptions } from "./parser-options.js"
6
+
7
+ type PositionalRepo = {
8
+ readonly positionalRepoUrl: string | undefined
9
+ readonly restArgs: ReadonlyArray<string>
10
+ }
11
+
12
+ export const resolveWorkspaceRepoPath = (
13
+ resolvedRepo: ReturnType<typeof resolveRepoInput>
14
+ ): string => {
15
+ const baseParts = deriveRepoPathParts(resolvedRepo.repoUrl).pathParts
16
+ const projectParts = resolvedRepo.workspaceSuffix ? [...baseParts, resolvedRepo.workspaceSuffix] : baseParts
17
+ return projectParts.join("/")
18
+ }
19
+
20
+ export const splitPositionalRepo = (args: ReadonlyArray<string>): PositionalRepo => {
21
+ const first = args[0]
22
+ const positionalRepoUrl = first !== undefined && !first.startsWith("-") ? first : undefined
23
+ const restArgs = positionalRepoUrl ? args.slice(1) : args
24
+ return { positionalRepoUrl, restArgs }
25
+ }
26
+
27
+ export const parseProjectDirWithOptions = (
28
+ args: ReadonlyArray<string>,
29
+ defaultProjectDir: string = "."
30
+ ): Either.Either<{ readonly projectDir: string; readonly raw: RawOptions }, ParseError> =>
31
+ Either.gen(function*(_) {
32
+ const { positionalRepoUrl, restArgs } = splitPositionalRepo(args)
33
+ const raw = yield* _(parseRawOptions(restArgs))
34
+ const rawRepoUrl = raw.repoUrl ?? positionalRepoUrl
35
+ const repoPath = rawRepoUrl ? resolveWorkspaceRepoPath(resolveRepoInput(rawRepoUrl)) : null
36
+ const projectDir = raw.projectDir ??
37
+ (repoPath
38
+ ? `.docker-git/${repoPath}`
39
+ : defaultProjectDir)
40
+
41
+ return { projectDir, raw }
42
+ })
43
+
44
+ export const parseProjectDirArgs = (
45
+ args: ReadonlyArray<string>,
46
+ defaultProjectDir: string = "."
47
+ ): Either.Either<{ readonly projectDir: string }, ParseError> =>
48
+ Either.map(
49
+ parseProjectDirWithOptions(args, defaultProjectDir),
50
+ ({ projectDir }) => ({ projectDir })
51
+ )