Skip to content

Commit dc6e3a7

Browse files
committed
fix(shell): satisfy lint rules for ssh access hints
1 parent af2e2f5 commit dc6e3a7

6 files changed

Lines changed: 78 additions & 34 deletions

File tree

packages/lib/src/usecases/actions/docker-up.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type * as CommandExecutor from "@effect/platform/CommandExecutor"
22
import type { PlatformError } from "@effect/platform/Error"
3-
import * as FileSystem from "@effect/platform/FileSystem"
4-
import * as Path from "@effect/platform/Path"
3+
import type * as FileSystem from "@effect/platform/FileSystem"
4+
import type * as Path from "@effect/platform/Path"
55
import { Duration, Effect, Fiber, Schedule } from "effect"
66

77
import type { CreateCommand } from "../../core/domain.js"

packages/lib/src/usecases/menu-helpers.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,31 +10,35 @@ export const isRepoUrlInput = (input: string): boolean => {
1010
trimmed.startsWith("git@")
1111
}
1212

13+
type ConnectionInfoOptions = {
14+
readonly authorizedKeysPath: string
15+
readonly authorizedKeysExists: boolean
16+
readonly sshCommand: string
17+
readonly editorAccessDetails?: string
18+
}
19+
1320
export const formatConnectionInfo = (
1421
cwd: string,
1522
config: ProjectConfig,
16-
authorizedKeysPath: string,
17-
authorizedKeysExists: boolean,
18-
sshCommand: string,
19-
editorAccessDetails?: string
23+
options: ConnectionInfoOptions
2024
): string => {
2125
const hostnameLabel = config.template.clonedOnHostname === undefined
2226
? ""
2327
: `\nCloned on device: ${config.template.clonedOnHostname}`
24-
const editorAccessLabel = editorAccessDetails === undefined ? "" : `\n${editorAccessDetails}`
28+
const editorAccessLabel = options.editorAccessDetails === undefined ? "" : `\n${options.editorAccessDetails}`
2529
return `Project directory: ${cwd}
2630
` +
2731
`Container: ${config.template.containerName}
2832
` +
2933
`Service: ${config.template.serviceName}
3034
` +
31-
`SSH command: ${sshCommand}
35+
`SSH command: ${options.sshCommand}
3236
` +
3337
`Repo: ${config.template.repoUrl} (${config.template.repoRef})
3438
` +
3539
`Workspace: ${config.template.targetDir}
3640
` +
37-
`Authorized keys: ${authorizedKeysPath}${authorizedKeysExists ? "" : " (missing)"}
41+
`Authorized keys: ${options.authorizedKeysPath}${options.authorizedKeysExists ? "" : " (missing)"}
3842
` +
3943
`Env global: ${config.template.envGlobalPath}
4044
` +

packages/lib/src/usecases/projects-core.ts

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,10 @@ import { findDockerGitConfigPaths } from "./docker-git-config-search.js"
1414
import { renderError } from "./errors.js"
1515
import { defaultProjectsRoot, formatConnectionInfo } from "./menu-helpers.js"
1616
import { findSshPrivateKey, resolveAuthorizedKeysPath, resolvePathFromCwd } from "./path-helpers.js"
17-
import { buildEditorSshAccess, buildSshCommand, formatEditorSshAccessDetails } from "./ssh-access.js"
1817
import { withFsPathContext } from "./runtime.js"
18+
import { buildEditorSshAccess, buildSshCommand, formatEditorSshAccessDetails } from "./ssh-access.js"
1919

2020
export type ProjectLoadError = PlatformError | ConfigNotFoundError | ConfigDecodeError
21-
export { buildSshCommand }
2221

2322
export type ProjectSummary = {
2423
readonly projectDir: string
@@ -148,13 +147,15 @@ export const renderProjectSummary = (summary: ProjectSummary): string =>
148147
formatConnectionInfo(
149148
summary.projectDir,
150149
summary.config,
151-
summary.authorizedKeysPath,
152-
summary.authorizedKeysExists,
153-
summary.sshCommand,
154-
formatEditorSshAccessDetails(
155-
buildEditorSshAccess(summary.config.template, summary.sshKeyPath, summary.ipAddress),
156-
summary.config.template.clonedOnHostname
157-
)
150+
{
151+
authorizedKeysPath: summary.authorizedKeysPath,
152+
authorizedKeysExists: summary.authorizedKeysExists,
153+
sshCommand: summary.sshCommand,
154+
editorAccessDetails: formatEditorSshAccessDetails(
155+
buildEditorSshAccess(summary.config.template, summary.sshKeyPath, summary.ipAddress),
156+
summary.config.template.clonedOnHostname
157+
)
158+
}
158159
)
159160

160161
const formatDisplayName = (repoUrl: string): string => {
@@ -309,3 +310,5 @@ export const withProjectIndexAndSsh = <A, E, R>(
309310
})
310311
)
311312
)
313+
314+
export { buildSshCommand } from "./ssh-access.js"

packages/lib/src/usecases/projects-ssh.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ import {
2525
renderProjectStatusHeader,
2626
withProjectIndexAndSsh
2727
} from "./projects-core.js"
28-
import { buildEditorSshAccess, formatEditorSshAccessSummary } from "./ssh-access.js"
2928
import { runDockerComposeUpWithPortCheck } from "./projects-up.js"
29+
import { buildEditorSshAccess, formatEditorSshAccessSummary } from "./ssh-access.js"
3030
import { ensureTerminalCursorVisible } from "./terminal-cursor.js"
3131

3232
const buildSshArgs = (item: ProjectItem): ReadonlyArray<string> => {

packages/lib/src/usecases/ssh-access.ts

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,43 @@ import { findSshPrivateKey, resolveAuthorizedKeysPath } from "./path-helpers.js"
1010

1111
const sshOptions = "-tt -Y -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"
1212

13+
const aliasCharPattern = /^[A-Za-z0-9._-]$/u
14+
15+
const isAliasChar = (value: string): boolean => aliasCharPattern.test(value)
16+
17+
const trimAliasEdges = (value: string): string => {
18+
let start = 0
19+
let end = value.length
20+
21+
while (start < end && (value[start] === "." || value[start] === "-")) {
22+
start += 1
23+
}
24+
25+
while (end > start && (value[end - 1] === "." || value[end - 1] === "-")) {
26+
end -= 1
27+
}
28+
29+
return value.slice(start, end)
30+
}
31+
1332
const sanitizeSshHostAlias = (value: string): string => {
14-
const normalized = value
15-
.trim()
16-
.replace(/[^A-Za-z0-9._-]+/g, "-")
17-
.replace(/-+/g, "-")
18-
.replace(/^[.-]+|[.-]+$/g, "")
33+
let normalized = ""
34+
let previousWasDash = false
35+
36+
for (const char of value.trim()) {
37+
if (isAliasChar(char)) {
38+
normalized += char
39+
previousWasDash = false
40+
continue
41+
}
42+
43+
if (!previousWasDash) {
44+
normalized += "-"
45+
previousWasDash = true
46+
}
47+
}
48+
49+
normalized = trimAliasEdges(normalized)
1950
return normalized.length === 0 ? "docker-git" : normalized
2051
}
2152

packages/lib/tests/usecases/connection-info.test.ts

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -29,27 +29,33 @@ const makeProjectConfig = (overrides: Partial<ProjectConfig["template"]> = {}):
2929
describe("formatConnectionInfo", () => {
3030
it("includes clonedOnHostname when present", () => {
3131
const config = makeProjectConfig({ clonedOnHostname: "my-laptop" })
32-
const output = formatConnectionInfo("/project", config, "/keys", true, "ssh dev@localhost")
32+
const output = formatConnectionInfo("/project", config, {
33+
authorizedKeysPath: "/keys",
34+
authorizedKeysExists: true,
35+
sshCommand: "ssh dev@localhost"
36+
})
3337
expect(output).toContain("Cloned on device: my-laptop")
3438
})
3539

3640
it("omits clonedOnHostname line when undefined", () => {
3741
const config = makeProjectConfig()
38-
const output = formatConnectionInfo("/project", config, "/keys", true, "ssh dev@localhost")
42+
const output = formatConnectionInfo("/project", config, {
43+
authorizedKeysPath: "/keys",
44+
authorizedKeysExists: true,
45+
sshCommand: "ssh dev@localhost"
46+
})
3947
expect(output).not.toContain("Cloned on device")
4048
})
4149

4250
it("includes Remote-SSH details when provided", () => {
4351
const config = makeProjectConfig({ clonedOnHostname: "meadav" })
4452
const editorAccess = buildEditorSshAccess(config.template, "/home/user/.ssh/id_ed25519")
45-
const output = formatConnectionInfo(
46-
"/project",
47-
config,
48-
"/keys",
49-
true,
50-
"ssh dev@localhost",
51-
formatEditorSshAccessDetails(editorAccess, config.template.clonedOnHostname)
52-
)
53+
const output = formatConnectionInfo("/project", config, {
54+
authorizedKeysPath: "/keys",
55+
authorizedKeysExists: true,
56+
sshCommand: "ssh dev@localhost",
57+
editorAccessDetails: formatEditorSshAccessDetails(editorAccess, config.template.clonedOnHostname)
58+
})
5359
expect(output).toContain("Remote-SSH host: dg-test")
5460
expect(output).toContain("Terminal shortcut: ssh dg-test")
5561
expect(output).toContain("VS Code/Cursor: Connect to Host... -> dg-test")

0 commit comments

Comments
 (0)