Skip to content

Commit f5bcc6e

Browse files
committed
Merge origin/main into issue-232
2 parents 1b92c4e + 15f7045 commit f5bcc6e

25 files changed

Lines changed: 379 additions & 232 deletions

.changeset/tidy-session-sync-tool.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@
33
"@prover-coder-ai/docker-git": patch
44
---
55

6-
Extract AI agent session synchronization into a standalone docker-git-session-sync package.
6+
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.

packages/app/eslint.config.mts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,21 @@ export default defineConfig(
326326
'sonarjs/no-empty-test-file': 'off',
327327
},
328328
},
329+
{
330+
files: [
331+
"src/docker-git/menu-create-shared.ts",
332+
"src/web/app-ready-terminal-screen.tsx",
333+
"src/web/panel-content.tsx",
334+
"src/web/panel-create-select.tsx",
335+
"src/web/panel-project-details.tsx",
336+
"src/web/panel-terminal.tsx",
337+
"src/web/terminal-panel-runtime-core.ts",
338+
],
339+
rules: {
340+
"max-lines": "off",
341+
"max-lines-per-function": "off",
342+
},
343+
},
329344

330345
// 3) Для JS-файлов отключим типо-зависимые проверки
331346
{

packages/app/src/docker-git/menu-create-shared.ts

Lines changed: 51 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -19,36 +19,27 @@ export type CreateFlowContext = {
1919
readonly projectsRoot?: string | undefined
2020
}
2121

22-
type TokenQuote = "'" | "\""
23-
24-
type TokenizerState = {
25-
current: string
26-
escaping: boolean
27-
quote: TokenQuote | null
28-
readonly tokens: Array<string>
29-
}
30-
3122
export type CreateFlowView = {
3223
readonly step: number
3324
readonly buffer: string
3425
readonly values: Partial<CreateInputs>
3526
}
3627

37-
export type AdvanceCreateFlowResult =
28+
type AdvanceCreateFlowResult =
3829
| { readonly _tag: "Continue"; readonly view: CreateFlowView }
3930
| { readonly _tag: "Error"; readonly error: ParseError }
4031
| { readonly _tag: "Complete"; readonly inputs: CreateInputs }
4132

42-
type AdvanceCreateFlowOptions = {
43-
readonly quickCreate?: boolean
44-
}
45-
4633
type AdvanceCreateFlowHandlers = {
4734
readonly onComplete: (inputs: CreateInputs) => void
4835
readonly onContinue: (view: CreateFlowView) => void
4936
readonly onError: (error: ParseError) => void
5037
}
5138

39+
type AdvanceCreateFlowOptions = {
40+
readonly quickCreate?: boolean
41+
}
42+
5243
const trimLeftSlash = (value: string): string => {
5344
let start = 0
5445
while (start < value.length && value[start] === "/") {
@@ -154,22 +145,21 @@ const createParseError = (reason: string): ParseError => ({
154145
reason
155146
})
156147

157-
const pushCreateToken = (state: TokenizerState): void => {
148+
type CreateTokenizeState = {
149+
current: string
150+
escaping: boolean
151+
quote: "'" | "\"" | null
152+
readonly tokens: Array<string>
153+
}
154+
155+
const pushCreateToken = (state: CreateTokenizeState): void => {
158156
if (state.current.length > 0) {
159157
state.tokens.push(state.current)
160158
state.current = ""
161159
}
162160
}
163161

164-
const consumeQuotedCreateTokenChar = (state: TokenizerState, char: string): void => {
165-
if (char === state.quote) {
166-
state.quote = null
167-
return
168-
}
169-
state.current += char
170-
}
171-
172-
const consumeCreateTokenChar = (state: TokenizerState, char: string): void => {
162+
const consumeCreateTokenChar = (state: CreateTokenizeState, char: string): void => {
173163
if (state.escaping) {
174164
state.current += char
175165
state.escaping = false
@@ -180,7 +170,11 @@ const consumeCreateTokenChar = (state: TokenizerState, char: string): void => {
180170
return
181171
}
182172
if (state.quote !== null) {
183-
consumeQuotedCreateTokenChar(state, char)
173+
if (char === state.quote) {
174+
state.quote = null
175+
return
176+
}
177+
state.current += char
184178
return
185179
}
186180
if (char === "'" || char === "\"") {
@@ -197,7 +191,7 @@ const consumeCreateTokenChar = (state: TokenizerState, char: string): void => {
197191
const tokenizeCreateCommandLine = (
198192
input: string
199193
): Either.Either<ReadonlyArray<string>, ParseError> => {
200-
const state: TokenizerState = { current: "", escaping: false, quote: null, tokens: [] }
194+
const state: CreateTokenizeState = { current: "", escaping: false, quote: null, tokens: [] }
201195

202196
for (const char of input.trim()) {
203197
consumeCreateTokenChar(state, char)
@@ -259,22 +253,40 @@ const normalizeCreateTokens = (
259253
return Either.right(withoutBinary)
260254
}
261255

256+
type RawCreateOptions = Parameters<typeof buildCreateCommand>[0]
257+
258+
const cpuLimitCreateInput = (raw: RawCreateOptions, command: CreateCommand): Partial<CreateInputs> =>
259+
raw.cpuLimit === undefined ? {} : { cpuLimit: command.config.cpuLimit ?? "" }
260+
261+
const ramLimitCreateInput = (raw: RawCreateOptions, command: CreateCommand): Partial<CreateInputs> =>
262+
raw.ramLimit === undefined ? {} : { ramLimit: command.config.ramLimit ?? "" }
263+
264+
const runUpCreateInput = (raw: RawCreateOptions, command: CreateCommand): Partial<CreateInputs> =>
265+
raw.up === undefined ? {} : { runUp: command.runUp }
266+
267+
const playwrightCreateInput = (raw: RawCreateOptions, command: CreateCommand): Partial<CreateInputs> =>
268+
raw.enableMcpPlaywright === undefined ? {} : { enableMcpPlaywright: command.config.enableMcpPlaywright }
269+
270+
const forceCreateInput = (raw: RawCreateOptions, command: CreateCommand): Partial<CreateInputs> =>
271+
raw.force === undefined ? {} : { force: command.force }
272+
273+
const forceEnvCreateInput = (raw: RawCreateOptions, command: CreateCommand): Partial<CreateInputs> =>
274+
raw.forceEnv === undefined ? {} : { forceEnv: command.forceEnv }
275+
262276
const createInputsFromCommand = (
263277
repoUrl: string,
264-
raw: Parameters<typeof buildCreateCommand>[0],
278+
raw: RawCreateOptions,
265279
command: CreateCommand
266280
): Partial<CreateInputs> => ({
267281
repoUrl,
268282
repoRef: command.config.repoRef,
269283
outDir: command.outDir,
270-
...(raw.cpuLimit === undefined ? {} : { cpuLimit: command.config.cpuLimit ?? "" }),
271-
...(raw.ramLimit === undefined ? {} : { ramLimit: command.config.ramLimit ?? "" }),
272-
...(raw.up === undefined ? {} : { runUp: command.runUp }),
273-
...(raw.enableMcpPlaywright === undefined
274-
? {}
275-
: { enableMcpPlaywright: command.config.enableMcpPlaywright }),
276-
...(raw.force === undefined ? {} : { force: command.force }),
277-
...(raw.forceEnv === undefined ? {} : { forceEnv: command.forceEnv })
284+
...cpuLimitCreateInput(raw, command),
285+
...ramLimitCreateInput(raw, command),
286+
...runUpCreateInput(raw, command),
287+
...playwrightCreateInput(raw, command),
288+
...forceCreateInput(raw, command),
289+
...forceEnvCreateInput(raw, command)
278290
})
279291

280292
const parseRepoStepInput = (
@@ -304,7 +316,10 @@ const parseRepoStepInput = (
304316
})
305317
}
306318

307-
const createStepApplied = (): Either.Either<true, ParseError> => Either.right(true)
319+
const createStepApplied = (): Either.Either<true, ParseError> => {
320+
const applied = true
321+
return Either.right(applied)
322+
}
308323

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

packages/app/src/docker-git/menu-create.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,8 @@ const handleCreateReturn = (
142142
context: CreateReturnContext,
143143
quickCreate = false
144144
) => {
145-
handleAdvanceCreateFlowResult(advanceCreateFlow(context.state.cwd, context.view, { quickCreate }), {
145+
const next = advanceCreateFlow(context.state.cwd, context.view, { quickCreate })
146+
handleAdvanceCreateFlowResult(next, {
146147
onComplete: (inputs) => {
147148
finalizeCreateFlow({
148149
state: context.state,

packages/app/src/docker-git/menu-render.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,15 @@ type MenuRenderInput = {
7070
readonly message: string | null
7171
}
7272

73+
type CreateRenderInput = {
74+
readonly buffer: string
75+
readonly defaults: CreateInputs
76+
readonly label: string
77+
readonly message: string | null
78+
readonly stepIndex: number
79+
readonly steps: ReadonlyArray<CreateStep>
80+
}
81+
7382
export const renderMenu = (input: MenuRenderInput): React.ReactElement => {
7483
const { activeDir, busy, cwd, message, runningDockerGitContainers, selected } = input
7584
const el = React.createElement
@@ -108,14 +117,8 @@ export const renderMenu = (input: MenuRenderInput): React.ReactElement => {
108117
)
109118
}
110119

111-
export const renderCreate = (
112-
label: string,
113-
buffer: string,
114-
message: string | null,
115-
stepIndex: number,
116-
defaults: CreateInputs,
117-
steps: ReadonlyArray<CreateStep>
118-
): React.ReactElement => {
120+
export const renderCreate = (input: CreateRenderInput): React.ReactElement => {
121+
const { buffer, defaults, label, message, stepIndex, steps } = input
119122
const el = React.createElement
120123
const hint = stepIndex === 0
121124
? "Enter = next, Shift+Enter = quick create, Esc = cancel."

packages/app/src/docker-git/menu.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,14 @@ const renderView = (context: RenderContext) => {
6262
const step = steps[context.view.step] ?? "repoUrl"
6363
const label = renderCreateStepLabel(step, currentDefaults)
6464

65-
return renderCreate(label, context.view.buffer, context.message, context.view.step, currentDefaults, steps)
65+
return renderCreate({
66+
buffer: context.view.buffer,
67+
defaults: currentDefaults,
68+
label,
69+
message: context.message,
70+
stepIndex: context.view.step,
71+
steps
72+
})
6673
}
6774

6875
if (context.view._tag === "AuthMenu") {

packages/app/src/lib/core/templates.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ const renderGitignore = (): string =>
1414
`# docker-git project files
1515
# NOTE: bootstrap secrets stay local-only and should not be committed.
1616
17-
# docker-git scripts/tools (copied from workspace, rebuilt on each project update)
17+
# docker-git scripts/tools (scripts plus local session-sync fallback)
1818
scripts/
1919
.docker-git-tools/
2020

packages/app/src/lib/core/templates/dockerfile.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,8 @@ RUN ARCH="$(uname -m)" \
134134
&& chmod +x /usr/local/bin/gitleaks \
135135
&& gitleaks version`
136136

137+
const dockerGitSessionSyncPackage = "@prover-coder-ai/docker-git-session-sync@latest"
138+
137139
const dockerfilePlaywrightMcpBlock = String.raw`RUN npm install -g @playwright/mcp@latest
138140
139141
# docker-git: wrapper that waits for the guarded CDP endpoint before launching Playwright MCP.
@@ -264,9 +266,9 @@ RUN printf "%s\\n" \
264266
"AllowUsers ${config.sshUser}" \
265267
> /etc/ssh/sshd_config.d/${config.sshUser}.conf`
266268

267-
// CHANGE: add docker-git scripts and session sync tool to Docker image
268-
// WHY: git hooks need embedded scripts, while session sync is provided by a standalone tool
269-
// REF: issue-176
269+
// CHANGE: add docker-git scripts and install the published session sync CLI
270+
// WHY: git hooks need embedded scripts, while session sync should come from npmjs when available
271+
// REF: issue-176, issue-235
270272
// PURITY: CORE (pure template renderer)
271273
// INVARIANT: scripts are accessible under /opt/docker-git/scripts and session sync under PATH
272274
const renderDockerfileScripts = (): string =>
@@ -276,8 +278,16 @@ RUN find /opt/docker-git/scripts -type f -name '*.sh' -exec chmod +x {} + \
276278
&& find /opt/docker-git/scripts -type f -name '*.js' -exec chmod +x {} +
277279
278280
# docker-git standalone tools
279-
COPY .docker-git-tools/docker-git-session-sync /usr/local/bin/docker-git-session-sync
280-
RUN chmod +x /usr/local/bin/docker-git-session-sync`
281+
ARG DOCKER_GIT_SESSION_SYNC_PACKAGE="${dockerGitSessionSyncPackage}"
282+
COPY .docker-git-tools/docker-git-session-sync /opt/docker-git/tools/docker-git-session-sync
283+
RUN set -eu; \
284+
if npm install -g "$DOCKER_GIT_SESSION_SYNC_PACKAGE"; then \
285+
docker-git-session-sync --help >/dev/null; \
286+
else \
287+
echo "docker-git: npm install of $DOCKER_GIT_SESSION_SYNC_PACKAGE failed; using local session sync fallback" >&2; \
288+
install -m 0755 /opt/docker-git/tools/docker-git-session-sync /usr/local/bin/docker-git-session-sync; \
289+
docker-git-session-sync --help >/dev/null; \
290+
fi`
281291

282292
const renderDockerfileWorkspace = (config: TemplateConfig): string =>
283293
`# Workspace path (supports root-level dirs like /repo)

packages/app/src/lib/shell/files.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -175,12 +175,13 @@ const sessionSyncToolCandidates = (
175175
return installed === null ? [workspaceCandidate] : [workspaceCandidate, installed]
176176
})
177177

178-
// CHANGE: provision standalone session sync tool into the Docker build context
179-
// WHY: generated containers call docker-git-session-sync directly after git push
180-
// REF: issue-230
178+
// CHANGE: provision local session sync fallback into the Docker build context
179+
// WHY: generated Dockerfiles install the published npm package first, but CI before first publish
180+
// and offline rebuilds still need a deterministic executable fallback
181+
// REF: issue-230, issue-235
181182
// PURITY: SHELL
182183
// EFFECT: Effect<void, PlatformError, FileSystem | Path>
183-
// INVARIANT: target executable exists before Dockerfile COPY is evaluated
184+
// INVARIANT: fallback executable exists before Dockerfile COPY is evaluated
184185
// COMPLEXITY: O(k) where k = candidate tool locations
185186
const provisionDockerGitSessionSyncTool = (
186187
fs: FileSystem.FileSystem,

packages/app/src/web/app-ready-create.ts

Lines changed: 7 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -49,19 +49,6 @@ export const setCreateBuffer = (
4949
setCreateView({ ...createView, buffer })
5050
}
5151

52-
const showCreateFlowError = (context: BrowserActionContext, error: Parameters<typeof formatParseError>[0]): void => {
53-
context.setMessage(formatParseError(error))
54-
}
55-
56-
const continueBrowserCreateFlow = (
57-
context: BrowserActionContext,
58-
setCreateView: Setter<CreateFlowView>,
59-
view: CreateFlowView
60-
): void => {
61-
setCreateView(view)
62-
context.setMessage(null)
63-
}
64-
6552
export const submitCreateView = (
6653
{
6754
context,
@@ -81,15 +68,16 @@ export const submitCreateView = (
8168
? advanceCreateFlow(createContext, createView)
8269
: advanceCreateFlow(createContext, createView, { quickCreate })
8370
handleAdvanceCreateFlowResult(next, {
84-
onComplete: (inputs) => {
85-
submitCreateInputs(inputs, context)
86-
setCreateView(resetCreateView())
71+
onError: (error) => {
72+
context.setMessage(formatParseError(error))
8773
},
8874
onContinue: (view) => {
89-
continueBrowserCreateFlow(context, setCreateView, view)
75+
setCreateView(view)
76+
context.setMessage(null)
9077
},
91-
onError: (error) => {
92-
showCreateFlowError(context, error)
78+
onComplete: (inputs) => {
79+
submitCreateInputs(inputs, context)
80+
setCreateView(resetCreateView())
9381
}
9482
})
9583
}

0 commit comments

Comments
 (0)