diff --git a/packages/app/src/docker-git/cli/parser-apply.ts b/packages/app/src/docker-git/cli/parser-apply.ts new file mode 100644 index 00000000..a3615eb9 --- /dev/null +++ b/packages/app/src/docker-git/cli/parser-apply.ts @@ -0,0 +1,28 @@ +import { Either } from "effect" + +import { type ApplyCommand, type ParseError } from "@effect-template/lib/core/domain" + +import { parseProjectDirWithOptions } from "./parser-shared.js" + +// CHANGE: parse "apply" command for existing docker-git projects +// WHY: update managed docker-git config on the current project/container without creating a new project +// QUOTE(ТЗ): "Не создавать новый... а прямо в текущем обновить её на актуальную" +// REF: issue-72-followup-apply-current-config +// SOURCE: n/a +// FORMAT THEOREM: forall argv: parseApply(argv) = cmd -> deterministic(cmd) +// PURITY: CORE +// EFFECT: Effect +// INVARIANT: projectDir is never empty +// COMPLEXITY: O(n) where n = |argv| +export const parseApply = ( + args: ReadonlyArray +): Either.Either => + Either.map(parseProjectDirWithOptions(args), ({ projectDir, raw }) => ({ + _tag: "Apply", + projectDir, + runUp: raw.up ?? true, + gitTokenLabel: raw.gitTokenLabel, + codexTokenLabel: raw.codexTokenLabel, + claudeTokenLabel: raw.claudeTokenLabel, + enableMcpPlaywright: raw.enableMcpPlaywright + })) diff --git a/packages/app/src/docker-git/cli/parser-options.ts b/packages/app/src/docker-git/cli/parser-options.ts index dd88fe7b..163ba7e0 100644 --- a/packages/app/src/docker-git/cli/parser-options.ts +++ b/packages/app/src/docker-git/cli/parser-options.ts @@ -24,6 +24,8 @@ interface ValueOptionSpec { | "scrapMode" | "label" | "gitTokenLabel" + | "codexTokenLabel" + | "claudeTokenLabel" | "token" | "scopes" | "message" @@ -53,6 +55,8 @@ const valueOptionSpecs: ReadonlyArray = [ { flag: "--mode", key: "scrapMode" }, { flag: "--label", key: "label" }, { flag: "--git-token", key: "gitTokenLabel" }, + { flag: "--codex-token", key: "codexTokenLabel" }, + { flag: "--claude-token", key: "claudeTokenLabel" }, { flag: "--token", key: "token" }, { flag: "--scopes", key: "scopes" }, { flag: "--message", key: "message" }, @@ -102,6 +106,8 @@ const valueFlagUpdaters: { readonly [K in ValueKey]: (raw: RawOptions, value: st scrapMode: (raw, value) => ({ ...raw, scrapMode: value }), label: (raw, value) => ({ ...raw, label: value }), gitTokenLabel: (raw, value) => ({ ...raw, gitTokenLabel: value }), + codexTokenLabel: (raw, value) => ({ ...raw, codexTokenLabel: value }), + claudeTokenLabel: (raw, value) => ({ ...raw, claudeTokenLabel: value }), token: (raw, value) => ({ ...raw, token: value }), scopes: (raw, value) => ({ ...raw, scopes: value }), message: (raw, value) => ({ ...raw, message: value }), @@ -129,47 +135,68 @@ export const applyCommandValueFlag = ( return Either.right(update(raw, value)) } -export const parseRawOptions = (args: ReadonlyArray): Either.Either => { - let index = 0 - let raw: RawOptions = {} +type ParseRawOptionsStep = + | { readonly _tag: "ok"; readonly raw: RawOptions; readonly nextIndex: number } + | { readonly _tag: "error"; readonly error: ParseError } - while (index < args.length) { - const token = args[index] ?? "" - const equalIndex = token.indexOf("=") - if (equalIndex > 0 && token.startsWith("-")) { - const flag = token.slice(0, equalIndex) - const inlineValue = token.slice(equalIndex + 1) - const nextRaw = applyCommandValueFlag(raw, flag, inlineValue) - if (Either.isLeft(nextRaw)) { - return Either.left(nextRaw.left) - } - raw = nextRaw.right - index += 1 - continue - } +const parseInlineValueToken = ( + raw: RawOptions, + token: string +): Either.Either | null => { + const equalIndex = token.indexOf("=") + if (equalIndex <= 0 || !token.startsWith("-")) { + return null + } - const booleanApplied = applyCommandBooleanFlag(raw, token) - if (booleanApplied !== null) { - raw = booleanApplied - index += 1 - continue - } + const flag = token.slice(0, equalIndex) + const inlineValue = token.slice(equalIndex + 1) + return applyCommandValueFlag(raw, flag, inlineValue) +} - if (!token.startsWith("-")) { - return Either.left({ _tag: "UnexpectedArgument", value: token }) - } +const parseRawOptionsStep = ( + args: ReadonlyArray, + index: number, + raw: RawOptions +): ParseRawOptionsStep => { + const token = args[index] ?? "" + const inlineApplied = parseInlineValueToken(raw, token) + if (inlineApplied !== null) { + return Either.isLeft(inlineApplied) + ? { _tag: "error", error: inlineApplied.left } + : { _tag: "ok", raw: inlineApplied.right, nextIndex: index + 1 } + } - const value = args[index + 1] - if (value === undefined) { - return Either.left({ _tag: "MissingOptionValue", option: token }) - } + const booleanApplied = applyCommandBooleanFlag(raw, token) + if (booleanApplied !== null) { + return { _tag: "ok", raw: booleanApplied, nextIndex: index + 1 } + } + + if (!token.startsWith("-")) { + return { _tag: "error", error: { _tag: "UnexpectedArgument", value: token } } + } + + const value = args[index + 1] + if (value === undefined) { + return { _tag: "error", error: { _tag: "MissingOptionValue", option: token } } + } + + const nextRaw = applyCommandValueFlag(raw, token, value) + return Either.isLeft(nextRaw) + ? { _tag: "error", error: nextRaw.left } + : { _tag: "ok", raw: nextRaw.right, nextIndex: index + 2 } +} + +export const parseRawOptions = (args: ReadonlyArray): Either.Either => { + let index = 0 + let raw: RawOptions = {} - const nextRaw = applyCommandValueFlag(raw, token, value) - if (Either.isLeft(nextRaw)) { - return Either.left(nextRaw.left) + while (index < args.length) { + const step = parseRawOptionsStep(args, index, raw) + if (step._tag === "error") { + return Either.left(step.error) } - raw = nextRaw.right - index += 2 + raw = step.raw + index = step.nextIndex } return Either.right(raw) diff --git a/packages/app/src/docker-git/cli/parser.ts b/packages/app/src/docker-git/cli/parser.ts index 8824f5ca..29d17423 100644 --- a/packages/app/src/docker-git/cli/parser.ts +++ b/packages/app/src/docker-git/cli/parser.ts @@ -2,6 +2,7 @@ import { Either, Match } from "effect" import { type Command, type ParseError } from "@effect-template/lib/core/domain" +import { parseApply } from "./parser-apply.js" import { parseAttach } from "./parser-attach.js" import { parseAuth } from "./parser-auth.js" import { parseClone } from "./parser-clone.js" @@ -74,6 +75,7 @@ export const parseArgs = (args: ReadonlyArray): Either.Either parseAuth(rest)) ) .pipe( + Match.when("apply", () => parseApply(rest)), Match.when("state", () => parseState(rest)), Match.orElse(() => Either.left(unknownCommandError)) ) diff --git a/packages/app/src/docker-git/cli/usage.ts b/packages/app/src/docker-git/cli/usage.ts index 2aceee40..69fcd86f 100644 --- a/packages/app/src/docker-git/cli/usage.ts +++ b/packages/app/src/docker-git/cli/usage.ts @@ -5,6 +5,7 @@ import type { ParseError } from "@effect-template/lib/core/domain" export const usageText = `docker-git menu docker-git create --repo-url [options] docker-git clone [options] +docker-git apply [] [options] docker-git mcp-playwright [] [options] docker-git attach [] [options] docker-git panes [] [options] @@ -21,6 +22,7 @@ Commands: menu Interactive menu (default when no args) create, init Generate docker development environment clone Create + run container and clone repo + apply Apply docker-git config to an existing project/container (current dir by default) mcp-playwright Enable Playwright MCP + Chromium sidecar for an existing project dir attach, tmux Open tmux workspace for a docker-git project panes, terms List tmux panes for a docker-git project @@ -50,6 +52,8 @@ Options: --archive Scrap snapshot directory (default: .orch/scrap/session) --mode Scrap mode (default: session) --git-token