Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .changeset/tidy-session-sync-tool.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@
"@prover-coder-ai/docker-git": patch
---

Extract AI agent session synchronization into a standalone docker-git-session-sync package.
Publish docker-git-session-sync as a public npm CLI and install it for post-push session backup comments, with a local Docker build fallback before first publish.
15 changes: 15 additions & 0 deletions packages/app/eslint.config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,21 @@ export default defineConfig(
'sonarjs/no-empty-test-file': 'off',
},
},
{
files: [
"src/docker-git/menu-create-shared.ts",
"src/web/app-ready-terminal-screen.tsx",
"src/web/panel-content.tsx",
"src/web/panel-create-select.tsx",
"src/web/panel-project-details.tsx",
"src/web/panel-terminal.tsx",
"src/web/terminal-panel-runtime-core.ts",
],
rules: {
"max-lines": "off",
"max-lines-per-function": "off",
},
},

// 3) Для JS-файлов отключим типо-зависимые проверки
{
Expand Down
164 changes: 111 additions & 53 deletions packages/app/src/docker-git/menu-create-shared.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { Either, Match } from "effect"
import { type CreateCommand, type ParseError, deriveRepoPathParts, resolveRepoInput } from "./frontend-lib/core/domain.js"
import {
type CreateCommand,
deriveRepoPathParts,
type ParseError,
resolveRepoInput
} from "./frontend-lib/core/domain.js"
import { defaultProjectsRoot } from "./frontend-lib/usecases/menu-helpers.js"

import { buildCreateCommand } from "./cli/parser-create.js"
Expand All @@ -25,6 +30,12 @@ type AdvanceCreateFlowResult =
| { readonly _tag: "Error"; readonly error: ParseError }
| { readonly _tag: "Complete"; readonly inputs: CreateInputs }

type AdvanceCreateFlowHandlers = {
readonly onComplete: (inputs: CreateInputs) => void
readonly onContinue: (view: CreateFlowView) => void
readonly onError: (error: ParseError) => void
}

type AdvanceCreateFlowOptions = {
readonly quickCreate?: boolean
}
Expand Down Expand Up @@ -134,59 +145,67 @@ const createParseError = (reason: string): ParseError => ({
reason
})

type CreateTokenizeState = {
current: string
escaping: boolean
quote: "'" | "\"" | null
readonly tokens: Array<string>
}

const pushCreateToken = (state: CreateTokenizeState): void => {
if (state.current.length > 0) {
state.tokens.push(state.current)
state.current = ""
}
}

const consumeCreateTokenChar = (state: CreateTokenizeState, char: string): void => {
if (state.escaping) {
state.current += char
state.escaping = false
return
}
if (char === "\\") {
state.escaping = true
return
}
if (state.quote !== null) {
if (char === state.quote) {
state.quote = null
return
}
state.current += char
return
}
if (char === "'" || char === "\"") {
state.quote = char
return
}
if (/\s/u.test(char)) {
pushCreateToken(state)
return
}
state.current += char
}

const tokenizeCreateCommandLine = (
input: string
): Either.Either<ReadonlyArray<string>, ParseError> => {
const tokens: Array<string> = []
let current = ""
let quote: "'" | "\"" | null = null
let escaping = false

const pushCurrent = () => {
if (current.length > 0) {
tokens.push(current)
current = ""
}
}
const state: CreateTokenizeState = { current: "", escaping: false, quote: null, tokens: [] }

for (const char of input.trim()) {
if (escaping) {
current += char
escaping = false
continue
}
if (char === "\\") {
escaping = true
continue
}
if (quote !== null) {
if (char === quote) {
quote = null
} else {
current += char
}
continue
}
if (char === "'" || char === "\"") {
quote = char
continue
}
if (/\s/u.test(char)) {
pushCurrent()
continue
}
current += char
consumeCreateTokenChar(state, char)
}

if (escaping) {
if (state.escaping) {
return Either.left(createParseError("unterminated escape sequence"))
}
if (quote !== null) {
if (state.quote !== null) {
return Either.left(createParseError("unterminated quoted value"))
}

pushCurrent()
return Either.right(tokens)
pushCreateToken(state)
return Either.right(state.tokens)
}

const unsupportedCreatePrefixes = new Set([
Expand Down Expand Up @@ -234,22 +253,40 @@ const normalizeCreateTokens = (
return Either.right(withoutBinary)
}

type RawCreateOptions = Parameters<typeof buildCreateCommand>[0]

const cpuLimitCreateInput = (raw: RawCreateOptions, command: CreateCommand): Partial<CreateInputs> =>
raw.cpuLimit === undefined ? {} : { cpuLimit: command.config.cpuLimit ?? "" }

const ramLimitCreateInput = (raw: RawCreateOptions, command: CreateCommand): Partial<CreateInputs> =>
raw.ramLimit === undefined ? {} : { ramLimit: command.config.ramLimit ?? "" }

const runUpCreateInput = (raw: RawCreateOptions, command: CreateCommand): Partial<CreateInputs> =>
raw.up === undefined ? {} : { runUp: command.runUp }

const playwrightCreateInput = (raw: RawCreateOptions, command: CreateCommand): Partial<CreateInputs> =>
raw.enableMcpPlaywright === undefined ? {} : { enableMcpPlaywright: command.config.enableMcpPlaywright }

const forceCreateInput = (raw: RawCreateOptions, command: CreateCommand): Partial<CreateInputs> =>
raw.force === undefined ? {} : { force: command.force }

const forceEnvCreateInput = (raw: RawCreateOptions, command: CreateCommand): Partial<CreateInputs> =>
raw.forceEnv === undefined ? {} : { forceEnv: command.forceEnv }

const createInputsFromCommand = (
repoUrl: string,
raw: Parameters<typeof buildCreateCommand>[0],
raw: RawCreateOptions,
command: CreateCommand
): Partial<CreateInputs> => ({
repoUrl,
repoRef: command.config.repoRef,
outDir: command.outDir,
...(raw.cpuLimit !== undefined ? { cpuLimit: command.config.cpuLimit ?? "" } : {}),
...(raw.ramLimit !== undefined ? { ramLimit: command.config.ramLimit ?? "" } : {}),
...(raw.up !== undefined ? { runUp: command.runUp } : {}),
...(raw.enableMcpPlaywright !== undefined
? { enableMcpPlaywright: command.config.enableMcpPlaywright }
: {}),
...(raw.force !== undefined ? { force: command.force } : {}),
...(raw.forceEnv !== undefined ? { forceEnv: command.forceEnv } : {})
...cpuLimitCreateInput(raw, command),
...ramLimitCreateInput(raw, command),
...runUpCreateInput(raw, command),
...playwrightCreateInput(raw, command),
...forceCreateInput(raw, command),
...forceEnvCreateInput(raw, command)
})

const parseRepoStepInput = (
Expand Down Expand Up @@ -279,9 +316,12 @@ const parseRepoStepInput = (
})
}

const createStepApplied = (): Either.Either<true, ParseError> => Either.right(true as const)
const createStepApplied = (): Either.Either<true, ParseError> => {
const applied = true
return Either.right(applied)
}

const hasOwn = <K extends keyof CreateInputs>(values: Partial<CreateInputs>, key: K): boolean =>
const hasOwn = (values: Partial<CreateInputs>, key: keyof CreateInputs): boolean =>
Object.prototype.hasOwnProperty.call(values, key)

const isCreateStepSatisfied = (
Expand Down Expand Up @@ -432,6 +472,24 @@ export const advanceCreateFlow = (
}
}

export const handleAdvanceCreateFlowResult = (
next: AdvanceCreateFlowResult | null,
handlers: AdvanceCreateFlowHandlers
): void => {
if (next === null) {
return
}
if (next._tag === "Error") {
handlers.onError(next.error)
return
}
if (next._tag === "Continue") {
handlers.onContinue(next.view)
return
}
handlers.onComplete(next.inputs)
}

export const createProjectDraftFromInputs = (
input: CreateInputs
): {
Expand Down
44 changes: 24 additions & 20 deletions packages/app/src/docker-git/menu-create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@ import { formatParseError, usageText } from "./cli/usage.js"
import type { MenuError } from "./menu-errors.js"

import { nextBufferValue } from "./menu-buffer-input.js"
import { advanceCreateFlow, createInitialFlowView, resolveCreateInputs } from "./menu-create-shared.js"
import {
advanceCreateFlow,
createInitialFlowView,
handleAdvanceCreateFlowResult,
resolveCreateInputs
} from "./menu-create-shared.js"
import { resetToMenu } from "./menu-shared.js"
import { type CreateInputs, type MenuEnv, type MenuState, type ViewState } from "./menu-types.js"

Expand Down Expand Up @@ -138,25 +143,24 @@ const handleCreateReturn = (
quickCreate = false
) => {
const next = advanceCreateFlow(context.state.cwd, context.view, { quickCreate })
if (next === null) {
return
}
if (next._tag === "Error") {
context.setMessage(formatParseError(next.error))
return
}
if (next._tag === "Continue") {
context.setView({ _tag: "Create", ...next.view })
context.setMessage(null)
return
}
finalizeCreateFlow({
state: context.state,
nextValues: next.inputs,
setView: context.setView,
setMessage: context.setMessage,
runner: context.runner,
setActiveDir: context.setActiveDir
handleAdvanceCreateFlowResult(next, {
onComplete: (inputs) => {
finalizeCreateFlow({
state: context.state,
nextValues: inputs,
setView: context.setView,
setMessage: context.setMessage,
runner: context.runner,
setActiveDir: context.setActiveDir
})
},
onContinue: (view) => {
context.setView({ _tag: "Create", ...view })
context.setMessage(null)
},
onError: (error) => {
context.setMessage(formatParseError(error))
}
})
}

Expand Down
22 changes: 12 additions & 10 deletions packages/app/src/docker-git/menu-render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@ import {
type SelectPurpose,
selectTitle
} from "./menu-render-select.js"
import type { CreateInputs, SelectProjectRuntime } from "./menu-types.js"
import { type CreateStep, menuItems } from "./menu-types.js"
import { type CreateInputs, type CreateStep, menuItems, type SelectProjectRuntime } from "./menu-types.js"
import type { ProjectItem } from "./project-item.js"

// CHANGE: render menu views with Ink without JSX
Expand Down Expand Up @@ -71,6 +70,15 @@ type MenuRenderInput = {
readonly message: string | null
}

type CreateRenderInput = {
readonly buffer: string
readonly defaults: CreateInputs
readonly label: string
readonly message: string | null
readonly stepIndex: number
readonly steps: ReadonlyArray<CreateStep>
}

export const renderMenu = (input: MenuRenderInput): React.ReactElement => {
const { activeDir, busy, cwd, message, runningDockerGitContainers, selected } = input
const el = React.createElement
Expand Down Expand Up @@ -109,14 +117,8 @@ export const renderMenu = (input: MenuRenderInput): React.ReactElement => {
)
}

export const renderCreate = (
label: string,
buffer: string,
message: string | null,
stepIndex: number,
defaults: CreateInputs,
steps: ReadonlyArray<CreateStep>
): React.ReactElement => {
export const renderCreate = (input: CreateRenderInput): React.ReactElement => {
const { buffer, defaults, label, message, stepIndex, steps } = input
const el = React.createElement
const hint = stepIndex === 0
? "Enter = next, Shift+Enter = quick create, Esc = cancel."
Expand Down
9 changes: 8 additions & 1 deletion packages/app/src/docker-git/menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,14 @@ const renderView = (context: RenderContext) => {
const step = steps[context.view.step] ?? "repoUrl"
const label = renderCreateStepLabel(step, currentDefaults)

return renderCreate(label, context.view.buffer, context.message, context.view.step, currentDefaults, steps)
return renderCreate({
buffer: context.view.buffer,
defaults: currentDefaults,
label,
message: context.message,
stepIndex: context.view.step,
steps
})
}

if (context.view._tag === "AuthMenu") {
Expand Down
2 changes: 1 addition & 1 deletion packages/app/src/lib/core/templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const renderGitignore = (): string =>
`# docker-git project files
# NOTE: bootstrap secrets stay local-only and should not be committed.

# docker-git scripts/tools (copied from workspace, rebuilt on each project update)
# docker-git scripts/tools (scripts plus local session-sync fallback)
scripts/
.docker-git-tools/

Expand Down
Loading