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 .github/actions/setup/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ inputs:
bun-version:
description: The version of Bun to install
required: true
default: 1.3.11
default: 1.3.14
node-version:
description: The version of Node.js to install for compatibility/native builds
required: true
Expand Down
174 changes: 86 additions & 88 deletions bun.lock

Large diffs are not rendered by default.

12 changes: 6 additions & 6 deletions packages/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@
"@effect/platform": "^0.96.1",
"@effect/platform-node": "^0.107.0",
"@effect/schema": "^0.75.5",
"@fedify/fedify": "^2.2.4",
"@fedify/vocab": "^2.2.4",
"effect": "^3.21.2",
"@fedify/fedify": "^2.2.5",
"@fedify/vocab": "^2.2.5",
"effect": "^3.21.3",
"node-pty": "^1.1.0",
"ws": "^8.21.0"
},
Expand All @@ -42,10 +42,10 @@
"devDependencies": {
"@effect/vitest": "^0.29.0",
"@eslint/js": "10.0.1",
"@types/node": "^25.9.1",
"@types/node": "^25.9.3",
"@types/ws": "^8.18.1",
"@typescript-eslint/eslint-plugin": "^8.60.1",
"@typescript-eslint/parser": "^8.60.1",
"@typescript-eslint/eslint-plugin": "^8.61.0",
"@typescript-eslint/parser": "^8.61.0",
"eslint": "^10.4.1",
"fast-check": "4.8.0",
"globals": "^17.6.0",
Expand Down
18 changes: 9 additions & 9 deletions packages/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@
"@effect/workflow": "^0.18.2",
"@gridland/bun": "0.4.3",
"@gridland/web": "0.4.3",
"effect": "^3.21.2",
"effect": "^3.21.3",
"react": "19.2.4",
"react-dom": "19.2.4",
"react-reconciler": "^0.33.0",
Expand All @@ -98,15 +98,15 @@
"@prover-coder-ai/docker-git-terminal": "workspace:*",
"@prover-coder-ai/eslint-plugin-suggest-members": "^0.0.26",
"@ton-ai-core/vibecode-linter": "^1.0.11",
"@types/node": "^25.9.1",
"@types/react": "^19.2.16",
"@types/node": "^25.9.3",
"@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3",
"@types/ws": "^8.18.1",
"@typescript-eslint/eslint-plugin": "^8.60.1",
"@typescript-eslint/parser": "^8.60.1",
"@typescript-eslint/eslint-plugin": "^8.61.0",
"@typescript-eslint/parser": "^8.61.0",
"@vitejs/plugin-react": "^6.0.2",
"@vitest/coverage-v8": "^4.1.8",
"@vitest/eslint-plugin": "^1.6.19",
"@vitest/eslint-plugin": "^1.6.20",
"biome": "npm:@biomejs/biome@^2.4.16",
"eslint": "^10.4.1",
"eslint-import-resolver-typescript": "^4.4.5",
Expand All @@ -115,12 +115,12 @@
"eslint-plugin-simple-import-sort": "^13.0.0",
"eslint-plugin-sonarjs": "^4.0.3",
"eslint-plugin-sort-destructure-keys": "^3.0.0",
"eslint-plugin-unicorn": "^64.0.0",
"eslint-plugin-unicorn": "^65.0.1",
"fast-check": "4.8.0",
"globals": "^17.6.0",
"jscpd": "^4.2.4",
"jscpd": "^5.0.8",
"typescript": "^6.0.3",
"typescript-eslint": "^8.60.1",
"typescript-eslint": "^8.61.0",
"vite": "^8.0.16",
"vitest": "^4.1.8",
"ws": "^8.21.0"
Expand Down
6 changes: 1 addition & 5 deletions packages/app/src/docker-git/api-container-tasks-codec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,7 @@ const readNumber = (value: JsonValue | undefined): number | null =>
const readBoolean = (value: JsonValue | undefined): boolean | null => typeof value === "boolean" ? value : null

const isTaskKind = (value: string): value is ApiContainerTaskKind =>
value === "ssh" ||
value === "web-terminal" ||
value === "agent" ||
value === "background" ||
value === "system"
["ssh", "web-terminal", "agent", "background", "system"].includes(value)

type DecodedContainerTaskFields = {
readonly command: string | null
Expand Down
2 changes: 1 addition & 1 deletion packages/app/src/docker-git/api-project-codec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ type RawProjectDetailFields = {

const isProjectStatus = (
value: string
): value is ApiProjectSummary["status"] => value === "running" || value === "stopped" || value === "unknown"
): value is ApiProjectSummary["status"] => ["running", "stopped", "unknown"].includes(value)

const stringOrEmpty = (value: string | null): string => value ?? ""

Expand Down
2 changes: 1 addition & 1 deletion packages/app/src/docker-git/api-terminal-codec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ type RawTerminalSession = {
const isTerminalSessionStatus = (
value: string
): value is ApiTerminalSession["status"] =>
value === "ready" || value === "attached" || value === "exited" || value === "failed"
["ready", "attached", "exited", "failed"].includes(value)

const readOptionalNumber = (value: JsonValue | undefined): number | undefined =>
typeof value === "number" ? value : undefined
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@ export const resolveAutoAgentFlags = (
if (requested === "auto") {
return Either.right({ agentMode: undefined, agentAuto: true })
}
if (requested === "claude" || requested === "codex" || requested === "gemini" || requested === "grok") {
return Either.right({ agentMode: requested, agentAuto: true })
const agentModes: readonly AgentMode[] = ["claude", "codex", "gemini", "grok"]
const matchedMode = agentModes.find((mode) => mode === requested)
if (matchedMode !== undefined) {
return Either.right({ agentMode: matchedMode, agentAuto: true })
}
return Either.left({
_tag: "InvalidOption",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import {
defaultTemplateConfig,
isDockerNetworkMode,
isGpuMode,
isUnixUserName,
isUnixUsername,
type ParseError,
sshUserNamePatternDescription
sshUsernamePatternDescription
} from "./domain.js"

const parsePort = (value: string): Either.Either<number, ParseError> => {
Expand Down Expand Up @@ -106,11 +106,11 @@ export const parseSshUser = (
option: "--ssh-user"
})
}
if (!isUnixUserName(candidate)) {
if (!isUnixUsername(candidate)) {
return Either.left({
_tag: "InvalidOption",
option: "--ssh-user",
reason: `expected Linux user name matching ${sshUserNamePatternDescription}`
reason: `expected Linux user name matching ${sshUsernamePatternDescription}`
})
}
return Either.right(candidate)
Expand Down
8 changes: 4 additions & 4 deletions packages/app/src/docker-git/frontend-lib/core/domain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,20 +64,20 @@ export type AgentMode = "claude" | "codex" | "gemini" | "grok"
export type DockerNetworkMode = "shared" | "project"
export type GpuMode = "none" | "all"

const unixUserNamePattern = /^[a-z_][a-z0-9_-]{0,31}$/
const unixUsernamePattern = /^[a-z_][a-z0-9_-]{0,31}$/

export const sshUserNamePatternDescription = "^[a-z_][a-z0-9_-]{0,31}$"
export const sshUsernamePatternDescription = "^[a-z_][a-z0-9_-]{0,31}$"

// CHANGE: define the SSH user name invariant in the core domain
// WHY: generated Dockerfiles and entrypoints interpolate sshUser into shell-sensitive user commands
// QUOTE(ТЗ): n/a
// REF: PR-281-coderabbit-sshUser-validation
// SOURCE: n/a
// FORMAT THEOREM: forall u: isUnixUserName(u) -> not contains_shell_metacharacters(u)
// FORMAT THEOREM: forall u: isUnixUsername(u) -> not contains_shell_metacharacters(u)
// PURITY: CORE
// INVARIANT: accepted user names contain only lowercase Linux account-name characters
// COMPLEXITY: O(n)/O(1) where n = |value|
export const isUnixUserName = (value: string): boolean => unixUserNamePattern.test(value)
export const isUnixUsername = (value: string): boolean => unixUsernamePattern.test(value)

export interface TemplateConfig {
readonly containerName: string
Expand Down
7 changes: 1 addition & 6 deletions packages/app/src/docker-git/host-errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,7 @@ export type HostError =
export type CliError = ParseError | HostError

const isParseError = (error: CliError): error is ParseError =>
error._tag === "UnknownCommand" ||
error._tag === "UnknownOption" ||
error._tag === "MissingOptionValue" ||
error._tag === "MissingRequiredOption" ||
error._tag === "InvalidOption" ||
error._tag === "UnexpectedArgument"
["UnknownCommand", "UnknownOption", "MissingOptionValue", "MissingRequiredOption", "InvalidOption", "UnexpectedArgument"].includes(error._tag)

const renderApiRequestError = (error: ApiRequestError): string =>
error.displayOnlyMessage === true
Expand Down
3 changes: 1 addition & 2 deletions packages/app/src/docker-git/menu-auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,7 @@ const submitAuthPrompt = (view: AuthPromptView, context: AuthInputContext) => {
const label = defaultLabel(nextValues["label"] ?? "")
const effect = resolveAuthPromptEffect(view, context.state.cwd, nextValues)
runAuthPromptEffect(effect, view, label, { ...context, cwd: context.state.cwd }, {
suspendTui: view.flow === "GithubOauth" || view.flow === "CodexOauth" || view.flow === "ClaudeOauth" ||
view.flow === "ClaudeLogout" || view.flow === "GeminiOauth" || view.flow === "GrokOauth"
suspendTui: ["GithubOauth", "CodexOauth", "ClaudeOauth", "ClaudeLogout", "GeminiOauth", "GrokOauth"].includes(view.flow)
})
}
)
Expand Down
6 changes: 1 addition & 5 deletions packages/app/src/docker-git/menu-project-auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,11 +130,7 @@ const runProjectAuthAction = (
}

if (
action === "ProjectGithubDisconnect" ||
action === "ProjectGitDisconnect" ||
action === "ProjectClaudeDisconnect" ||
action === "ProjectGeminiDisconnect" ||
action === "ProjectGrokDisconnect"
["ProjectGithubDisconnect", "ProjectGitDisconnect", "ProjectClaudeDisconnect", "ProjectGeminiDisconnect", "ProjectGrokDisconnect"].includes(action)
) {
runProjectAuthEffect(view.project, action, {}, "default", context)
return
Expand Down
6 changes: 4 additions & 2 deletions packages/app/src/lib/core/auto-agent-flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@ export const resolveAutoAgentFlags = (
if (requested === "auto") {
return Either.right({ agentMode: undefined, agentAuto: true })
}
if (requested === "claude" || requested === "codex" || requested === "gemini" || requested === "grok") {
return Either.right({ agentMode: requested, agentAuto: true })
const agentModes: readonly AgentMode[] = ["claude", "codex", "gemini", "grok"]
const matchedMode = agentModes.find((mode) => mode === requested)
if (matchedMode !== undefined) {
return Either.right({ agentMode: matchedMode, agentAuto: true })
}
return Either.left({
_tag: "InvalidOption",
Expand Down
8 changes: 4 additions & 4 deletions packages/app/src/lib/core/command-builders-shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import {
defaultTemplateConfig,
isDockerNetworkMode,
isGpuMode,
isUnixUserName,
isUnixUsername,
type ParseError,
sshUserNamePatternDescription
sshUsernamePatternDescription
} from "./domain.js"

const parsePort = (value: string): Either.Either<number, ParseError> => {
Expand Down Expand Up @@ -106,11 +106,11 @@ export const parseSshUser = (
option: "--ssh-user"
})
}
if (!isUnixUserName(candidate)) {
if (!isUnixUsername(candidate)) {
return Either.left({
_tag: "InvalidOption",
option: "--ssh-user",
reason: `expected Linux user name matching ${sshUserNamePatternDescription}`
reason: `expected Linux user name matching ${sshUsernamePatternDescription}`
})
}
return Either.right(candidate)
Expand Down
8 changes: 4 additions & 4 deletions packages/app/src/lib/core/domain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,20 +64,20 @@ export type AgentMode = "claude" | "codex" | "gemini" | "grok"
export type DockerNetworkMode = "shared" | "project"
export type GpuMode = "none" | "all"

const unixUserNamePattern = /^[a-z_][a-z0-9_-]{0,31}$/
const unixUsernamePattern = /^[a-z_][a-z0-9_-]{0,31}$/

export const sshUserNamePatternDescription = "^[a-z_][a-z0-9_-]{0,31}$"
export const sshUsernamePatternDescription = "^[a-z_][a-z0-9_-]{0,31}$"

// CHANGE: define the SSH user name invariant in the core domain
// WHY: generated Dockerfiles and entrypoints interpolate sshUser into shell-sensitive user commands
// QUOTE(ТЗ): n/a
// REF: PR-281-coderabbit-sshUser-validation
// SOURCE: n/a
// FORMAT THEOREM: forall u: isUnixUserName(u) -> not contains_shell_metacharacters(u)
// FORMAT THEOREM: forall u: isUnixUsername(u) -> not contains_shell_metacharacters(u)
// PURITY: CORE
// INVARIANT: accepted user names contain only lowercase Linux account-name characters
// COMPLEXITY: O(n)/O(1) where n = |value|
export const isUnixUserName = (value: string): boolean => unixUserNamePattern.test(value)
export const isUnixUsername = (value: string): boolean => unixUsernamePattern.test(value)

export interface TemplateConfig {
readonly containerName: string
Expand Down
8 changes: 4 additions & 4 deletions packages/app/src/lib/shell/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ import { Effect, Either } from "effect"

import {
defaultTemplateConfig,
isUnixUserName,
isUnixUsername,
type ProjectConfig,
sshUserNamePatternDescription
sshUsernamePatternDescription
} from "../core/domain.js"
import { ConfigDecodeError, ConfigNotFoundError } from "./errors.js"
import { resolveBaseDir } from "./paths.js"
Expand Down Expand Up @@ -102,12 +102,12 @@ const validateProjectConfig = (
path: string,
config: ProjectConfig
): Effect.Effect<ProjectConfig, ConfigDecodeError> =>
isUnixUserName(config.template.sshUser)
isUnixUsername(config.template.sshUser)
? Effect.succeed(config)
: Effect.fail(
new ConfigDecodeError({
path,
message: `template.sshUser must match ${sshUserNamePatternDescription}`
message: `template.sshUser must match ${sshUsernamePatternDescription}`
})
)

Expand Down
7 changes: 1 addition & 6 deletions packages/app/src/lib/usecases/docker-git-config-search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,7 @@ type DockerGitConfigSearchState = {
const isDockerGitConfig = (entry: string): boolean => entry.endsWith("docker-git.json")

const shouldSkipDir = (entry: string): boolean =>
entry === ".git" ||
entry === ".orch" ||
entry === ".docker-git" ||
entry === ".cache" ||
entry === "node_modules" ||
entry === "tmp"
[".git", ".orch", ".docker-git", ".cache", "node_modules", "tmp"].includes(entry)

const isNotFoundStatError = (error: PlatformError): boolean =>
error._tag === "SystemError" && error.reason === "NotFound"
Expand Down
7 changes: 1 addition & 6 deletions packages/app/src/lib/usecases/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,7 @@ export type AppError =
type NonParseError = Exclude<AppError, ParseError>

const isParseError = (error: AppError): error is ParseError =>
error._tag === "UnknownCommand" ||
error._tag === "UnknownOption" ||
error._tag === "MissingOptionValue" ||
error._tag === "MissingRequiredOption" ||
error._tag === "InvalidOption" ||
error._tag === "UnexpectedArgument"
["UnknownCommand", "UnknownOption", "MissingOptionValue", "MissingRequiredOption", "InvalidOption", "UnexpectedArgument"].includes(error._tag)

const renderDockerAccessHeadline = (issue: DockerAccessError["issue"]): string =>
issue === "PermissionDenied"
Expand Down
2 changes: 1 addition & 1 deletion packages/app/src/lib/usecases/gitlab-token-preflight.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ const mapGitlabRepoAccessStatus = (status: number): GitlabRepoAccessStatus => {
if (status >= 200 && status < 300) {
return "accessible"
}
if (status === 401 || status === 403 || status === 404) {
if ([401, 403, 404].includes(status)) {
return "notAccessible"
}
return "unknown"
Expand Down
4 changes: 2 additions & 2 deletions packages/app/src/lib/usecases/state-repo/env.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
/* jscpd:ignore-start */
export const isTruthyEnv = (value: string): boolean => {
const normalized = value.trim().toLowerCase()
return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on"
return ["1", "true", "yes", "on"].includes(normalized)
}

export const isFalsyEnv = (value: string): boolean => {
const normalized = value.trim().toLowerCase()
return normalized === "0" || normalized === "false" || normalized === "no" || normalized === "off"
return ["0", "false", "no", "off"].includes(normalized)
}

export const autoPullEnvKey = "DOCKER_GIT_STATE_AUTO_PULL"
Expand Down
6 changes: 3 additions & 3 deletions packages/app/src/web/actions-auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,13 +184,13 @@ const openProjectAuthPrompt = (

const isMenuNavigationAction = (
action: AuthMenuAction | ProjectAuthMenuAction
): action is "Back" | "Refresh" => action === "Back" || action === "Refresh"
): action is "Back" | "Refresh" => ["Back", "Refresh"].includes(action)

const isCodexAuthAction = (action: BrowserAuthPrompt["action"]): action is CodexAuthAction =>
action === "CodexOauth" || action === "CodexLogout"
["CodexOauth", "CodexLogout"].includes(action)

const isTerminalOnlyAuthAction = (action: BrowserAuthPrompt["action"]): action is TerminalAuthFlow =>
action === "ClaudeOauth" || action === "GeminiOauth" || action === "GrokOauth"
["ClaudeOauth", "GeminiOauth", "GrokOauth"].includes(action)

const runCodexAuthAction = (
action: CodexAuthAction,
Expand Down
2 changes: 1 addition & 1 deletion packages/app/src/web/app-ready-terminal-storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ const readJsonArray = (value: JsonValue | undefined): ReadonlyArray<JsonValue> |
const isStoredTerminalStatus = (
value: string | null
): value is ActiveTerminalSession["session"]["status"] =>
value === "ready" || value === "attached" || value === "exited" || value === "failed"
value !== null && ["ready", "attached", "exited", "failed"].includes(value)

type StoredTerminalSessionFields = {
readonly createdAt: string | null
Expand Down
4 changes: 1 addition & 3 deletions packages/app/src/web/app-ready-url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,9 +117,7 @@ const resolveProjectId = (
return null
}
const project = projects.find((candidate) =>
candidate.id === normalizedToken ||
candidate.projectKey === normalizedToken ||
candidate.displayName === normalizedToken
[candidate.id, candidate.projectKey, candidate.displayName].includes(normalizedToken)
)
return project?.id ?? null
}
Expand Down
Loading
Loading