refactor(reference): normalize config entries (#28178)
This commit is contained in:
@@ -19,7 +19,7 @@ import type { PromptInfo } from "./history"
|
|||||||
import { useFrecency } from "./frecency"
|
import { useFrecency } from "./frecency"
|
||||||
import { useBindings } from "../../keymap"
|
import { useBindings } from "../../keymap"
|
||||||
import { Reference } from "@/reference/reference"
|
import { Reference } from "@/reference/reference"
|
||||||
import type { Config } from "@/config/config"
|
import { ConfigReference } from "@/config/reference"
|
||||||
import { displayCharAt, mentionTriggerIndex } from "@/cli/cmd/prompt-display"
|
import { displayCharAt, mentionTriggerIndex } from "@/cli/cmd/prompt-display"
|
||||||
|
|
||||||
function removeLineRange(input: string) {
|
function removeLineRange(input: string) {
|
||||||
@@ -310,7 +310,7 @@ export function Autocomplete(props: {
|
|||||||
`Referenced configured reference @${reference.name}.`,
|
`Referenced configured reference @${reference.name}.`,
|
||||||
...(reference.kind === "local" ? ["Kind: local directory"] : []),
|
...(reference.kind === "local" ? ["Kind: local directory"] : []),
|
||||||
...(reference.kind === "git" ? ["Kind: git repository"] : []),
|
...(reference.kind === "git" ? ["Kind: git repository"] : []),
|
||||||
...(reference.kind === "invalid" ? [`Repository: ${reference.repository}`] : []),
|
...(reference.kind === "invalid" && reference.repository ? [`Repository: ${reference.repository}`] : []),
|
||||||
...(reference.kind === "git" ? [`Repository: ${reference.repository}`] : []),
|
...(reference.kind === "git" ? [`Repository: ${reference.repository}`] : []),
|
||||||
...(reference.kind === "git" && reference.branch ? [`Branch/ref: ${reference.branch}`] : []),
|
...(reference.kind === "git" && reference.branch ? [`Branch/ref: ${reference.branch}`] : []),
|
||||||
...(reference.kind === "invalid" ? [] : [`Reference root: ${reference.path}`]),
|
...(reference.kind === "invalid" ? [] : [`Reference root: ${reference.path}`]),
|
||||||
@@ -324,7 +324,7 @@ export function Autocomplete(props: {
|
|||||||
|
|
||||||
const references = createMemo(() =>
|
const references = createMemo(() =>
|
||||||
Reference.resolveAll({
|
Reference.resolveAll({
|
||||||
references: (sync.data.config.reference ?? {}) as NonNullable<Config.Info["reference"]>,
|
references: ConfigReference.normalize(sync.data.config.reference ?? {}),
|
||||||
directory: sync.path.directory || process.cwd(),
|
directory: sync.path.directory || process.cwd(),
|
||||||
worktree: sync.path.worktree || sync.path.directory || process.cwd(),
|
worktree: sync.path.worktree || sync.path.directory || process.cwd(),
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -18,6 +18,52 @@ const Local = Schema.Struct({
|
|||||||
})
|
})
|
||||||
|
|
||||||
export const Entry = Schema.Union([Schema.String, Git, Local]).annotate({ identifier: "ReferenceConfigEntry" })
|
export const Entry = Schema.Union([Schema.String, Git, Local]).annotate({ identifier: "ReferenceConfigEntry" })
|
||||||
|
export type Entry = Schema.Schema.Type<typeof Entry>
|
||||||
|
|
||||||
export const Info = Schema.Record(Schema.String, Entry).annotate({ identifier: "ReferenceConfig" })
|
export const Info = Schema.Record(Schema.String, Entry).annotate({ identifier: "ReferenceConfig" })
|
||||||
export type Info = Schema.Schema.Type<typeof Info>
|
export type Info = Schema.Schema.Type<typeof Info>
|
||||||
|
|
||||||
|
export type NormalizedEntry =
|
||||||
|
| {
|
||||||
|
kind: "local"
|
||||||
|
path: string
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
kind: "git"
|
||||||
|
repository: string
|
||||||
|
branch?: string
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
kind: "invalid"
|
||||||
|
message: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NormalizedInfo = Record<string, NormalizedEntry>
|
||||||
|
|
||||||
|
export function validateAlias(name: string) {
|
||||||
|
if (name.length === 0) return "Reference alias must not be empty"
|
||||||
|
if (/[\/\s`,]/.test(name)) {
|
||||||
|
return "Reference alias must not contain /, whitespace, comma, or backtick"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeEntry(entry: Entry): NormalizedEntry {
|
||||||
|
if (typeof entry === "string") {
|
||||||
|
if (entry.startsWith(".") || entry.startsWith("/") || entry.startsWith("~")) {
|
||||||
|
return { kind: "local", path: entry }
|
||||||
|
}
|
||||||
|
return { kind: "git", repository: entry }
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("path" in entry) return { kind: "local", path: entry.path }
|
||||||
|
return { kind: "git", repository: entry.repository, branch: entry.branch }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalize(info: Info): NormalizedInfo {
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(info).map(([name, entry]) => {
|
||||||
|
const aliasError = validateAlias(name)
|
||||||
|
return [name, aliasError ? { kind: "invalid" as const, message: aliasError } : normalizeEntry(entry)] as const
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,14 +3,13 @@ import { Effect, Context, Layer, Scope } from "effect"
|
|||||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||||
import { Global } from "@opencode-ai/core/global"
|
import { Global } from "@opencode-ai/core/global"
|
||||||
import { Config } from "@/config/config"
|
import { Config } from "@/config/config"
|
||||||
|
import { ConfigReference } from "@/config/reference"
|
||||||
import { InstanceState } from "@/effect/instance-state"
|
import { InstanceState } from "@/effect/instance-state"
|
||||||
import { RuntimeFlags } from "@/effect/runtime-flags"
|
import { RuntimeFlags } from "@/effect/runtime-flags"
|
||||||
import { Git } from "@/git"
|
import { Git } from "@/git"
|
||||||
import { parseRepositoryReference, repositoryCachePath, type Reference as RepositoryReference } from "@/util/repository"
|
import { parseRepositoryReference, repositoryCachePath, type Reference as RepositoryReference } from "@/util/repository"
|
||||||
import { RepositoryCache } from "./repository-cache"
|
import { RepositoryCache } from "./repository-cache"
|
||||||
|
|
||||||
type ReferenceEntry = NonNullable<Config.Info["reference"]>[string]
|
|
||||||
|
|
||||||
export type Resolved =
|
export type Resolved =
|
||||||
| {
|
| {
|
||||||
name: string
|
name: string
|
||||||
@@ -28,7 +27,7 @@ export type Resolved =
|
|||||||
| {
|
| {
|
||||||
name: string
|
name: string
|
||||||
kind: "invalid"
|
kind: "invalid"
|
||||||
repository: string
|
repository?: string
|
||||||
message: string
|
message: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,26 +91,21 @@ function containsReferencePath(referencePath: string, target: string) {
|
|||||||
|
|
||||||
export function resolve(input: {
|
export function resolve(input: {
|
||||||
name: string
|
name: string
|
||||||
reference: ReferenceEntry
|
reference: ConfigReference.NormalizedEntry
|
||||||
directory: string
|
directory: string
|
||||||
worktree: string
|
worktree: string
|
||||||
}): Resolved {
|
}): Resolved {
|
||||||
if (typeof input.reference === "string") {
|
if (input.reference.kind === "invalid") {
|
||||||
if (input.reference.startsWith(".") || input.reference.startsWith("/") || input.reference.startsWith("~")) {
|
return { name: input.name, kind: "invalid", message: input.reference.message }
|
||||||
return { name: input.name, kind: "local", path: referencePath({ ...input, value: input.reference }) }
|
|
||||||
}
|
|
||||||
return resolveGit({ name: input.name, repository: input.reference })
|
|
||||||
}
|
}
|
||||||
|
if (input.reference.kind === "local") {
|
||||||
if ("path" in input.reference) {
|
|
||||||
return { name: input.name, kind: "local", path: referencePath({ ...input, value: input.reference.path }) }
|
return { name: input.name, kind: "local", path: referencePath({ ...input, value: input.reference.path }) }
|
||||||
}
|
}
|
||||||
|
|
||||||
return resolveGit({ name: input.name, repository: input.reference.repository, branch: input.reference.branch })
|
return resolveGit({ name: input.name, repository: input.reference.repository, branch: input.reference.branch })
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveAll(input: {
|
export function resolveAll(input: {
|
||||||
references: NonNullable<Config.Info["reference"]>
|
references: ConfigReference.NormalizedInfo
|
||||||
directory: string
|
directory: string
|
||||||
worktree: string
|
worktree: string
|
||||||
}) {
|
}) {
|
||||||
@@ -149,7 +143,7 @@ export const layer = Layer.effect(
|
|||||||
Effect.fn("Reference.state")(function* (ctx) {
|
Effect.fn("Reference.state")(function* (ctx) {
|
||||||
const cfg = yield* config.get()
|
const cfg = yield* config.get()
|
||||||
const references = resolveAll({
|
const references = resolveAll({
|
||||||
references: cfg.reference ?? {},
|
references: ConfigReference.normalize(cfg.reference ?? {}),
|
||||||
directory: ctx.directory,
|
directory: ctx.directory,
|
||||||
worktree: ctx.worktree,
|
worktree: ctx.worktree,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
|||||||
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
||||||
import { Global } from "@opencode-ai/core/global"
|
import { Global } from "@opencode-ai/core/global"
|
||||||
import { Config } from "../../src/config/config"
|
import { Config } from "../../src/config/config"
|
||||||
|
import { ConfigReference } from "../../src/config/reference"
|
||||||
import { RuntimeFlags } from "../../src/effect/runtime-flags"
|
import { RuntimeFlags } from "../../src/effect/runtime-flags"
|
||||||
import { Git } from "../../src/git"
|
import { Git } from "../../src/git"
|
||||||
import { Reference } from "../../src/reference/reference"
|
import { Reference } from "../../src/reference/reference"
|
||||||
@@ -86,25 +87,25 @@ describe("reference", () => {
|
|||||||
const root = path.resolve("opencode-reference-root")
|
const root = path.resolve("opencode-reference-root")
|
||||||
const local = Reference.resolve({
|
const local = Reference.resolve({
|
||||||
name: "docs",
|
name: "docs",
|
||||||
reference: { path: "../docs" },
|
reference: ConfigReference.normalizeEntry({ path: "../docs" }),
|
||||||
directory: path.join(root, "packages", "app"),
|
directory: path.join(root, "packages", "app"),
|
||||||
worktree: root,
|
worktree: root,
|
||||||
})
|
})
|
||||||
const repo = Reference.resolve({
|
const repo = Reference.resolve({
|
||||||
name: "effect",
|
name: "effect",
|
||||||
reference: { repository: "Effect-TS/effect", branch: "main" },
|
reference: ConfigReference.normalizeEntry({ repository: "Effect-TS/effect", branch: "main" }),
|
||||||
directory: path.join(root, "packages", "app"),
|
directory: path.join(root, "packages", "app"),
|
||||||
worktree: root,
|
worktree: root,
|
||||||
})
|
})
|
||||||
const localString = Reference.resolve({
|
const localString = Reference.resolve({
|
||||||
name: "notes",
|
name: "notes",
|
||||||
reference: "./notes",
|
reference: ConfigReference.normalizeEntry("./notes"),
|
||||||
directory: path.join(root, "packages", "app"),
|
directory: path.join(root, "packages", "app"),
|
||||||
worktree: root,
|
worktree: root,
|
||||||
})
|
})
|
||||||
const repoString = Reference.resolve({
|
const repoString = Reference.resolve({
|
||||||
name: "repo",
|
name: "repo",
|
||||||
reference: "owner/repo",
|
reference: ConfigReference.normalizeEntry("owner/repo"),
|
||||||
directory: path.join(root, "packages", "app"),
|
directory: path.join(root, "packages", "app"),
|
||||||
worktree: root,
|
worktree: root,
|
||||||
})
|
})
|
||||||
@@ -159,11 +160,11 @@ describe("reference", () => {
|
|||||||
const references = Reference.resolveAll({
|
const references = Reference.resolveAll({
|
||||||
directory: root,
|
directory: root,
|
||||||
worktree: root,
|
worktree: root,
|
||||||
references: {
|
references: ConfigReference.normalize({
|
||||||
main: { repository: "owner/repo", branch: "main" },
|
main: { repository: "owner/repo", branch: "main" },
|
||||||
dev: { repository: "github.com/owner/repo", branch: "dev" },
|
dev: { repository: "github.com/owner/repo", branch: "dev" },
|
||||||
alsoMain: { repository: "https://github.com/owner/repo", branch: "main" },
|
alsoMain: { repository: "https://github.com/owner/repo", branch: "main" },
|
||||||
},
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(references.map((reference) => reference.kind)).toEqual(["git", "invalid", "git"])
|
expect(references.map((reference) => reference.kind)).toEqual(["git", "invalid", "git"])
|
||||||
@@ -175,6 +176,27 @@ describe("reference", () => {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
it.live("represents invalid aliases as invalid references", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const root = path.resolve("opencode-reference-root")
|
||||||
|
const references = Reference.resolveAll({
|
||||||
|
directory: root,
|
||||||
|
worktree: root,
|
||||||
|
references: ConfigReference.normalize({
|
||||||
|
"bad/name": "owner/repo",
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(references).toEqual([
|
||||||
|
{
|
||||||
|
name: "bad/name",
|
||||||
|
kind: "invalid",
|
||||||
|
message: "Reference alias must not contain /, whitespace, comma, or backtick",
|
||||||
|
},
|
||||||
|
])
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
scout.live("materializes configured git references during init", () =>
|
scout.live("materializes configured git references during init", () =>
|
||||||
provideTmpdirInstance(
|
provideTmpdirInstance(
|
||||||
(_dir) =>
|
(_dir) =>
|
||||||
|
|||||||
Reference in New Issue
Block a user