瀏覽代碼

feat(app): add workspace startup script to projects

Adam 2 月之前
父節點
當前提交
16fad51b5e

+ 15 - 0
packages/app/src/components/dialog-edit-project.tsx

@@ -25,6 +25,7 @@ export function DialogEditProject(props: { project: LocalProject }) {
     name: defaultName(),
     color: props.project.icon?.color || "pink",
     iconUrl: props.project.icon?.override || "",
+    startup: props.project.commands?.start ?? "",
     saving: false,
   })
 
@@ -69,15 +70,18 @@ export function DialogEditProject(props: { project: LocalProject }) {
 
   async function handleSubmit(e: SubmitEvent) {
     e.preventDefault()
+
     if (!props.project.id) return
 
     setStore("saving", true)
     const name = store.name.trim() === folderName() ? "" : store.name.trim()
+    const start = store.startup.trim()
     await globalSDK.client.project.update({
       projectID: props.project.id,
       directory: props.project.worktree,
       name,
       icon: { color: store.color, override: store.iconUrl },
+      commands: { start },
     })
     setStore("saving", false)
     dialog.close()
@@ -215,6 +219,17 @@ export function DialogEditProject(props: { project: LocalProject }) {
               </div>
             </div>
           </Show>
+
+          <TextField
+            multiline
+            label={language.t("dialog.project.edit.worktree.startup")}
+            description={language.t("dialog.project.edit.worktree.startup.description")}
+            placeholder={language.t("dialog.project.edit.worktree.startup.placeholder")}
+            value={store.startup}
+            onChange={(v) => setStore("startup", v)}
+            spellcheck={false}
+            class="max-h-40 w-full font-mono text-xs no-scrollbar"
+          />
         </div>
 
         <div class="flex justify-end gap-2">

+ 3 - 0
packages/app/src/i18n/en.ts

@@ -257,6 +257,9 @@ export const dict = {
   "dialog.project.edit.icon.recommended": "Recommended: 128x128px",
   "dialog.project.edit.color": "Color",
   "dialog.project.edit.color.select": "Select {{color}} color",
+  "dialog.project.edit.worktree.startup": "Workspace startup script",
+  "dialog.project.edit.worktree.startup.description": "Runs after creating a new workspace (worktree).",
+  "dialog.project.edit.worktree.startup.placeholder": "e.g. bun install",
 
   "context.breakdown.title": "Context Breakdown",
   "context.breakdown.note": 'Approximate breakdown of input tokens. "Other" includes tool definitions and overhead.',

+ 16 - 0
packages/opencode/src/project/project.ts

@@ -29,6 +29,11 @@ export namespace Project {
           color: z.string().optional(),
         })
         .optional(),
+      commands: z
+        .object({
+          start: z.string().optional().describe("Startup script to run when creating a new workspace (worktree)"),
+        })
+        .optional(),
       time: z.object({
         created: z.number(),
         updated: z.number(),
@@ -287,6 +292,7 @@ export namespace Project {
       projectID: z.string(),
       name: z.string().optional(),
       icon: Info.shape.icon.optional(),
+      commands: Info.shape.commands.optional(),
     }),
     async (input) => {
       const result = await Storage.update<Info>(["project", input.projectID], (draft) => {
@@ -299,6 +305,16 @@ export namespace Project {
           if (input.icon.override !== undefined) draft.icon.override = input.icon.override || undefined
           if (input.icon.color !== undefined) draft.icon.color = input.icon.color
         }
+
+        if (input.commands?.start !== undefined) {
+          const start = input.commands.start || undefined
+          draft.commands = {
+            ...(draft.commands ?? {}),
+          }
+          draft.commands.start = start
+          if (!draft.commands.start) draft.commands = undefined
+        }
+
         draft.time.updated = Date.now()
       })
       GlobalBus.emit("event", {

+ 1 - 1
packages/opencode/src/server/routes/experimental.ts

@@ -90,7 +90,7 @@ export const ExperimentalRoutes = lazy(() =>
       "/worktree",
       describeRoute({
         summary: "Create worktree",
-        description: "Create a new git worktree for the current project.",
+        description: "Create a new git worktree for the current project and run any configured startup scripts.",
         operationId: "worktree.create",
         responses: {
           200: {

+ 1 - 1
packages/opencode/src/server/routes/project.ts

@@ -56,7 +56,7 @@ export const ProjectRoutes = lazy(() =>
       "/:projectID",
       describeRoute({
         summary: "Update project",
-        description: "Update project properties such as name, icon and color.",
+        description: "Update project properties such as name, icon, and commands.",
         operationId: "project.update",
         responses: {
           200: {

+ 21 - 6
packages/opencode/src/worktree/index.ts

@@ -6,6 +6,7 @@ import { NamedError } from "@opencode-ai/util/error"
 import { Global } from "../global"
 import { Instance } from "../project/instance"
 import { Project } from "../project/project"
+import { Storage } from "../storage/storage"
 import { fn } from "../util/fn"
 import { Config } from "@/config/config"
 
@@ -25,7 +26,10 @@ export namespace Worktree {
   export const CreateInput = z
     .object({
       name: z.string().optional(),
-      startCommand: z.string().optional(),
+      startCommand: z
+        .string()
+        .optional()
+        .describe("Additional startup script to run after the project's start command"),
     })
     .meta({
       ref: "WorktreeCreateInput",
@@ -238,12 +242,23 @@ export namespace Worktree {
       throw new CreateFailedError({ message: errorText(created) || "Failed to create git worktree" })
     }
 
-    const cmd = input?.startCommand?.trim()
-    if (!cmd) return info
+    const project = await Storage.read<Project.Info>(["project", Instance.project.id]).catch(() => Instance.project)
+    const startup = project.commands?.start?.trim()
+    if (startup) {
+      const ran = await runStartCommand(info.directory, startup)
+      if (ran.exitCode !== 0) {
+        throw new StartCommandFailedError({
+          message: errorText(ran) || "Project start command failed",
+        })
+      }
+    }
 
-    const ran = await runStartCommand(info.directory, cmd)
-    if (ran.exitCode !== 0) {
-      throw new StartCommandFailedError({ message: errorText(ran) || "Worktree start command failed" })
+    const extra = input?.startCommand?.trim()
+    if (extra) {
+      const ran = await runStartCommand(info.directory, extra)
+      if (ran.exitCode !== 0) {
+        throw new StartCommandFailedError({ message: errorText(ran) || "Worktree start command failed" })
+      }
     }
 
     return info

+ 9 - 2
packages/sdk/js/src/v2/gen/sdk.gen.ts

@@ -293,7 +293,7 @@ export class Project extends HeyApiClient {
   /**
    * Update project
    *
-   * Update project properties such as name, icon and color.
+   * Update project properties such as name, icon, and commands.
    */
   public update<ThrowOnError extends boolean = false>(
     parameters: {
@@ -305,6 +305,12 @@ export class Project extends HeyApiClient {
         override?: string
         color?: string
       }
+      commands?: {
+        /**
+         * Startup script to run when creating a new workspace (worktree)
+         */
+        start?: string
+      }
     },
     options?: Options<never, ThrowOnError>,
   ) {
@@ -317,6 +323,7 @@ export class Project extends HeyApiClient {
             { in: "query", key: "directory" },
             { in: "body", key: "name" },
             { in: "body", key: "icon" },
+            { in: "body", key: "commands" },
           ],
         },
       ],
@@ -718,7 +725,7 @@ export class Worktree extends HeyApiClient {
   /**
    * Create worktree
    *
-   * Create a new git worktree for the current project.
+   * Create a new git worktree for the current project and run any configured startup scripts.
    */
   public create<ThrowOnError extends boolean = false>(
     parameters?: {

+ 15 - 0
packages/sdk/js/src/v2/gen/types.gen.ts

@@ -28,6 +28,12 @@ export type Project = {
     override?: string
     color?: string
   }
+  commands?: {
+    /**
+     * Startup script to run when creating a new workspace (worktree)
+     */
+    start?: string
+  }
   time: {
     created: number
     updated: number
@@ -1906,6 +1912,9 @@ export type Worktree = {
 
 export type WorktreeCreateInput = {
   name?: string
+  /**
+   * Additional startup script to run after the project's start command
+   */
   startCommand?: string
 }
 
@@ -2233,6 +2242,12 @@ export type ProjectUpdateData = {
       override?: string
       color?: string
     }
+    commands?: {
+      /**
+       * Startup script to run when creating a new workspace (worktree)
+       */
+      start?: string
+    }
   }
   path: {
     projectID: string

文件差異過大導致無法顯示
+ 365 - 87
packages/sdk/openapi.json


部分文件因文件數量過多而無法顯示