Просмотр исходного кода

refactor: unwrap Question namespace + fix script to emit "." for index.ts (#22992)

Kit Langton 1 день назад
Родитель
Сommit
4f8986aa48

+ 8 - 3
packages/opencode/script/unwrap-and-self-reexport.ts

@@ -207,10 +207,15 @@ const rewrittenBody = dedented.map(rewriteLine)
 
 // Assemble the new file. Collapse multiple trailing blank lines so the
 // self-reexport sits cleanly at the end.
+//
+// When the file is itself `index.ts`, prefer `"."` over `"./index"` — both are
+// valid but `"."` matches the existing convention in the codebase (e.g.
+// pty/index.ts, file/index.ts, etc.) and avoids referencing "index" literally.
 const basename = path.basename(absPath, ".ts")
+const reexportSource = basename === "index" ? "." : `./${basename}`
 const assembled = [...before, ...rewrittenBody, ...after].join("\n")
 const trimmed = assembled.replace(/\s+$/g, "")
-const output = `${trimmed}\n\nexport * as ${nsName} from "./${basename}"\n`
+const output = `${trimmed}\n\nexport * as ${nsName} from "${reexportSource}"\n`
 
 if (dryRun) {
   console.log(`--- dry run: ${path.relative(process.cwd(), absPath)} ---`)
@@ -218,7 +223,7 @@ if (dryRun) {
   console.log(`body lines:     ${body.length}`)
   console.log(`declared names: ${Array.from(declaredNames).join(", ") || "(none)"}`)
   console.log(`self-refs rewr: ${rewriteCount}`)
-  console.log(`self-reexport:  export * as ${nsName} from "./${basename}"`)
+  console.log(`self-reexport:  export * as ${nsName} from "${reexportSource}"`)
   console.log(`output preview (last 10 lines):`)
   const outputLines = output.split("\n")
   for (const l of outputLines.slice(Math.max(0, outputLines.length - 10))) {
@@ -231,7 +236,7 @@ fs.writeFileSync(absPath, output)
 console.log(`unwrapped ${path.relative(process.cwd(), absPath)} → ${nsName}`)
 console.log(`  body lines:      ${body.length}`)
 console.log(`  self-refs rewr:  ${rewriteCount}`)
-console.log(`  self-reexport:   export * as ${nsName} from "./${basename}"`)
+console.log(`  self-reexport:   export * as ${nsName} from "${reexportSource}"`)
 console.log("")
 console.log("Next: verify with")
 console.log("  bunx --bun tsgo --noEmit")

+ 194 - 194
packages/opencode/src/question/index.ts

@@ -8,222 +8,222 @@ import { Log } from "@/util"
 import { withStatics } from "@/util/schema"
 import { QuestionID } from "./schema"
 
-export namespace Question {
-  const log = Log.create({ service: "question" })
-
-  // Schemas
-
-  export class Option extends Schema.Class<Option>("QuestionOption")({
-    label: Schema.String.annotate({
-      description: "Display text (1-5 words, concise)",
-    }),
-    description: Schema.String.annotate({
-      description: "Explanation of choice",
-    }),
-  }) {
-    static readonly zod = zod(this)
-  }
+const log = Log.create({ service: "question" })
+
+// Schemas
+
+export class Option extends Schema.Class<Option>("QuestionOption")({
+  label: Schema.String.annotate({
+    description: "Display text (1-5 words, concise)",
+  }),
+  description: Schema.String.annotate({
+    description: "Explanation of choice",
+  }),
+}) {
+  static readonly zod = zod(this)
+}
 
-  const base = {
-    question: Schema.String.annotate({
-      description: "Complete question",
-    }),
-    header: Schema.String.annotate({
-      description: "Very short label (max 30 chars)",
-    }),
-    options: Schema.Array(Option).annotate({
-      description: "Available choices",
-    }),
-    multiple: Schema.optional(Schema.Boolean).annotate({
-      description: "Allow selecting multiple choices",
-    }),
-  }
+const base = {
+  question: Schema.String.annotate({
+    description: "Complete question",
+  }),
+  header: Schema.String.annotate({
+    description: "Very short label (max 30 chars)",
+  }),
+  options: Schema.Array(Option).annotate({
+    description: "Available choices",
+  }),
+  multiple: Schema.optional(Schema.Boolean).annotate({
+    description: "Allow selecting multiple choices",
+  }),
+}
 
-  export class Info extends Schema.Class<Info>("QuestionInfo")({
-    ...base,
-    custom: Schema.optional(Schema.Boolean).annotate({
-      description: "Allow typing a custom answer (default: true)",
-    }),
-  }) {
-    static readonly zod = zod(this)
-  }
+export class Info extends Schema.Class<Info>("QuestionInfo")({
+  ...base,
+  custom: Schema.optional(Schema.Boolean).annotate({
+    description: "Allow typing a custom answer (default: true)",
+  }),
+}) {
+  static readonly zod = zod(this)
+}
 
-  export class Prompt extends Schema.Class<Prompt>("QuestionPrompt")(base) {
-    static readonly zod = zod(this)
-  }
+export class Prompt extends Schema.Class<Prompt>("QuestionPrompt")(base) {
+  static readonly zod = zod(this)
+}
 
-  export class Tool extends Schema.Class<Tool>("QuestionTool")({
-    messageID: MessageID,
-    callID: Schema.String,
-  }) {
-    static readonly zod = zod(this)
-  }
+export class Tool extends Schema.Class<Tool>("QuestionTool")({
+  messageID: MessageID,
+  callID: Schema.String,
+}) {
+  static readonly zod = zod(this)
+}
 
-  export class Request extends Schema.Class<Request>("QuestionRequest")({
-    id: QuestionID,
-    sessionID: SessionID,
-    questions: Schema.Array(Info).annotate({
-      description: "Questions to ask",
-    }),
-    tool: Schema.optional(Tool),
-  }) {
-    static readonly zod = zod(this)
-  }
+export class Request extends Schema.Class<Request>("QuestionRequest")({
+  id: QuestionID,
+  sessionID: SessionID,
+  questions: Schema.Array(Info).annotate({
+    description: "Questions to ask",
+  }),
+  tool: Schema.optional(Tool),
+}) {
+  static readonly zod = zod(this)
+}
 
-  export const Answer = Schema.Array(Schema.String)
-    .annotate({ identifier: "QuestionAnswer" })
-    .pipe(withStatics((s) => ({ zod: zod(s) })))
-  export type Answer = Schema.Schema.Type<typeof Answer>
-
-  export class Reply extends Schema.Class<Reply>("QuestionReply")({
-    answers: Schema.Array(Answer).annotate({
-      description: "User answers in order of questions (each answer is an array of selected labels)",
-    }),
-  }) {
-    static readonly zod = zod(this)
-  }
+export const Answer = Schema.Array(Schema.String)
+  .annotate({ identifier: "QuestionAnswer" })
+  .pipe(withStatics((s) => ({ zod: zod(s) })))
+export type Answer = Schema.Schema.Type<typeof Answer>
+
+export class Reply extends Schema.Class<Reply>("QuestionReply")({
+  answers: Schema.Array(Answer).annotate({
+    description: "User answers in order of questions (each answer is an array of selected labels)",
+  }),
+}) {
+  static readonly zod = zod(this)
+}
 
-  class Replied extends Schema.Class<Replied>("QuestionReplied")({
-    sessionID: SessionID,
-    requestID: QuestionID,
-    answers: Schema.Array(Answer),
-  }) {}
-
-  class Rejected extends Schema.Class<Rejected>("QuestionRejected")({
-    sessionID: SessionID,
-    requestID: QuestionID,
-  }) {}
-
-  export const Event = {
-    Asked: BusEvent.define("question.asked", Request.zod),
-    Replied: BusEvent.define("question.replied", zod(Replied)),
-    Rejected: BusEvent.define("question.rejected", zod(Rejected)),
-  }
+class Replied extends Schema.Class<Replied>("QuestionReplied")({
+  sessionID: SessionID,
+  requestID: QuestionID,
+  answers: Schema.Array(Answer),
+}) {}
+
+class Rejected extends Schema.Class<Rejected>("QuestionRejected")({
+  sessionID: SessionID,
+  requestID: QuestionID,
+}) {}
+
+export const Event = {
+  Asked: BusEvent.define("question.asked", Request.zod),
+  Replied: BusEvent.define("question.replied", zod(Replied)),
+  Rejected: BusEvent.define("question.rejected", zod(Rejected)),
+}
 
-  export class RejectedError extends Schema.TaggedErrorClass<RejectedError>()("QuestionRejectedError", {}) {
-    override get message() {
-      return "The user dismissed this question"
-    }
+export class RejectedError extends Schema.TaggedErrorClass<RejectedError>()("QuestionRejectedError", {}) {
+  override get message() {
+    return "The user dismissed this question"
   }
+}
 
-  interface PendingEntry {
-    info: Request
-    deferred: Deferred.Deferred<ReadonlyArray<Answer>, RejectedError>
-  }
+interface PendingEntry {
+  info: Request
+  deferred: Deferred.Deferred<ReadonlyArray<Answer>, RejectedError>
+}
 
-  interface State {
-    pending: Map<QuestionID, PendingEntry>
-  }
+interface State {
+  pending: Map<QuestionID, PendingEntry>
+}
+
+// Service
+
+export interface Interface {
+  readonly ask: (input: {
+    sessionID: SessionID
+    questions: ReadonlyArray<Info>
+    tool?: Tool
+  }) => Effect.Effect<ReadonlyArray<Answer>, RejectedError>
+  readonly reply: (input: { requestID: QuestionID; answers: ReadonlyArray<Answer> }) => Effect.Effect<void>
+  readonly reject: (requestID: QuestionID) => Effect.Effect<void>
+  readonly list: () => Effect.Effect<ReadonlyArray<Request>>
+}
+
+export class Service extends Context.Service<Service, Interface>()("@opencode/Question") {}
+
+export const layer = Layer.effect(
+  Service,
+  Effect.gen(function* () {
+    const bus = yield* Bus.Service
+    const state = yield* InstanceState.make<State>(
+      Effect.fn("Question.state")(function* () {
+        const state = {
+          pending: new Map<QuestionID, PendingEntry>(),
+        }
 
-  // Service
+        yield* Effect.addFinalizer(() =>
+          Effect.gen(function* () {
+            for (const item of state.pending.values()) {
+              yield* Deferred.fail(item.deferred, new RejectedError())
+            }
+            state.pending.clear()
+          }),
+        )
 
-  export interface Interface {
-    readonly ask: (input: {
+        return state
+      }),
+    )
+
+    const ask = Effect.fn("Question.ask")(function* (input: {
       sessionID: SessionID
       questions: ReadonlyArray<Info>
       tool?: Tool
-    }) => Effect.Effect<ReadonlyArray<Answer>, RejectedError>
-    readonly reply: (input: { requestID: QuestionID; answers: ReadonlyArray<Answer> }) => Effect.Effect<void>
-    readonly reject: (requestID: QuestionID) => Effect.Effect<void>
-    readonly list: () => Effect.Effect<ReadonlyArray<Request>>
-  }
+    }) {
+      const pending = (yield* InstanceState.get(state)).pending
+      const id = QuestionID.ascending()
+      log.info("asking", { id, questions: input.questions.length })
+
+      const deferred = yield* Deferred.make<ReadonlyArray<Answer>, RejectedError>()
+      const info = Schema.decodeUnknownSync(Request)({
+        id,
+        sessionID: input.sessionID,
+        questions: input.questions,
+        tool: input.tool,
+      })
+      pending.set(id, { info, deferred })
+      yield* bus.publish(Event.Asked, info)
 
-  export class Service extends Context.Service<Service, Interface>()("@opencode/Question") {}
-
-  export const layer = Layer.effect(
-    Service,
-    Effect.gen(function* () {
-      const bus = yield* Bus.Service
-      const state = yield* InstanceState.make<State>(
-        Effect.fn("Question.state")(function* () {
-          const state = {
-            pending: new Map<QuestionID, PendingEntry>(),
-          }
-
-          yield* Effect.addFinalizer(() =>
-            Effect.gen(function* () {
-              for (const item of state.pending.values()) {
-                yield* Deferred.fail(item.deferred, new RejectedError())
-              }
-              state.pending.clear()
-            }),
-          )
-
-          return state
+      return yield* Effect.ensuring(
+        Deferred.await(deferred),
+        Effect.sync(() => {
+          pending.delete(id)
         }),
       )
-
-      const ask = Effect.fn("Question.ask")(function* (input: {
-        sessionID: SessionID
-        questions: ReadonlyArray<Info>
-        tool?: Tool
-      }) {
-        const pending = (yield* InstanceState.get(state)).pending
-        const id = QuestionID.ascending()
-        log.info("asking", { id, questions: input.questions.length })
-
-        const deferred = yield* Deferred.make<ReadonlyArray<Answer>, RejectedError>()
-        const info = Schema.decodeUnknownSync(Request)({
-          id,
-          sessionID: input.sessionID,
-          questions: input.questions,
-          tool: input.tool,
-        })
-        pending.set(id, { info, deferred })
-        yield* bus.publish(Event.Asked, info)
-
-        return yield* Effect.ensuring(
-          Deferred.await(deferred),
-          Effect.sync(() => {
-            pending.delete(id)
-          }),
-        )
+    })
+
+    const reply = Effect.fn("Question.reply")(function* (input: {
+      requestID: QuestionID
+      answers: ReadonlyArray<Answer>
+    }) {
+      const pending = (yield* InstanceState.get(state)).pending
+      const existing = pending.get(input.requestID)
+      if (!existing) {
+        log.warn("reply for unknown request", { requestID: input.requestID })
+        return
+      }
+      pending.delete(input.requestID)
+      log.info("replied", { requestID: input.requestID, answers: input.answers })
+      yield* bus.publish(Event.Replied, {
+        sessionID: existing.info.sessionID,
+        requestID: existing.info.id,
+        answers: input.answers,
       })
-
-      const reply = Effect.fn("Question.reply")(function* (input: {
-        requestID: QuestionID
-        answers: ReadonlyArray<Answer>
-      }) {
-        const pending = (yield* InstanceState.get(state)).pending
-        const existing = pending.get(input.requestID)
-        if (!existing) {
-          log.warn("reply for unknown request", { requestID: input.requestID })
-          return
-        }
-        pending.delete(input.requestID)
-        log.info("replied", { requestID: input.requestID, answers: input.answers })
-        yield* bus.publish(Event.Replied, {
-          sessionID: existing.info.sessionID,
-          requestID: existing.info.id,
-          answers: input.answers,
-        })
-        yield* Deferred.succeed(existing.deferred, input.answers)
+      yield* Deferred.succeed(existing.deferred, input.answers)
+    })
+
+    const reject = Effect.fn("Question.reject")(function* (requestID: QuestionID) {
+      const pending = (yield* InstanceState.get(state)).pending
+      const existing = pending.get(requestID)
+      if (!existing) {
+        log.warn("reject for unknown request", { requestID })
+        return
+      }
+      pending.delete(requestID)
+      log.info("rejected", { requestID })
+      yield* bus.publish(Event.Rejected, {
+        sessionID: existing.info.sessionID,
+        requestID: existing.info.id,
       })
+      yield* Deferred.fail(existing.deferred, new RejectedError())
+    })
 
-      const reject = Effect.fn("Question.reject")(function* (requestID: QuestionID) {
-        const pending = (yield* InstanceState.get(state)).pending
-        const existing = pending.get(requestID)
-        if (!existing) {
-          log.warn("reject for unknown request", { requestID })
-          return
-        }
-        pending.delete(requestID)
-        log.info("rejected", { requestID })
-        yield* bus.publish(Event.Rejected, {
-          sessionID: existing.info.sessionID,
-          requestID: existing.info.id,
-        })
-        yield* Deferred.fail(existing.deferred, new RejectedError())
-      })
+    const list = Effect.fn("Question.list")(function* () {
+      const pending = (yield* InstanceState.get(state)).pending
+      return Array.from(pending.values(), (x) => x.info)
+    })
 
-      const list = Effect.fn("Question.list")(function* () {
-        const pending = (yield* InstanceState.get(state)).pending
-        return Array.from(pending.values(), (x) => x.info)
-      })
+    return Service.of({ ask, reply, reject, list })
+  }),
+)
 
-      return Service.of({ ask, reply, reject, list })
-    }),
-  )
+export const defaultLayer = layer.pipe(Layer.provide(Bus.layer))
 
-  export const defaultLayer = layer.pipe(Layer.provide(Bus.layer))
-}
+export * as Question from "."