( ports: WorkflowPorts, store: ProgressStore, cwdOverride?: string, runsDirProvider: () => string = getRunsDir, )
| 120 | * @param runsDirProvider For tests only: inject a tmpdir (Bun ESM module namespace is read-only, cannot monkey-patch getRunsDir). |
| 121 | */ |
| 122 | export function makeService( |
| 123 | ports: WorkflowPorts, |
| 124 | store: ProgressStore, |
| 125 | cwdOverride?: string, |
| 126 | runsDirProvider: () => string = getRunsDir, |
| 127 | ): WorkflowService { |
| 128 | const buildHost = ( |
| 129 | toolUseContext: ToolUseContext, |
| 130 | canUseTool: CanUseToolFn, |
| 131 | ): WorkflowHostContext => ({ |
| 132 | handle: makeHostHandle(buildHostBundle(toolUseContext, canUseTool)), |
| 133 | // Use projectRoot to stay in sync with ports.ts hostFactory / journalStore; |
| 134 | // entering a worktree/subdirectory will not desync named workflow resolution from journal persistence. |
| 135 | // cwdOverride is for tests only: inject a temp directory (avoids inline persistence writing to the real project directory). |
| 136 | cwd: cwdOverride ?? getProjectRoot(), |
| 137 | budgetTotal: null, // turn-level budget injection point (in future read from settings) |
| 138 | toolUseId: toolUseContext.toolUseId, |
| 139 | }) |
| 140 | |
| 141 | async function resolveSource(input: { |
| 142 | script?: string |
| 143 | name?: string |
| 144 | scriptPath?: string |
| 145 | title?: string |
| 146 | }): Promise<{ |
| 147 | script: string |
| 148 | workflowFile?: string |
| 149 | workflowName: string |
| 150 | }> { |
| 151 | // Mirrors WorkflowTool.ts: name takes priority over title; only fall back to the literal |
| 152 | // 'workflow' when neither is supplied (so /workflows tabs don't pile up under a same default name). |
| 153 | const workflowName = input.name ?? input.title ?? 'workflow' |
| 154 | if (input.script) { |
| 155 | return { script: input.script, workflowName } |
| 156 | } |
| 157 | if (input.scriptPath) { |
| 158 | return { |
| 159 | script: await readFile(input.scriptPath, 'utf-8'), |
| 160 | workflowFile: input.scriptPath, |
| 161 | workflowName, |
| 162 | } |
| 163 | } |
| 164 | if (input.name) { |
| 165 | const dir = join(getProjectRoot(), WORKFLOW_DIR_NAME) |
| 166 | const found = await resolveNamedWorkflow(dir, input.name) |
| 167 | if (!found) { |
| 168 | throw new Error( |
| 169 | `Named workflow "${input.name}" not found (looked in ${WORKFLOW_DIR_NAME}/)`, |
| 170 | ) |
| 171 | } |
| 172 | return { |
| 173 | script: found.content, |
| 174 | workflowFile: found.path, |
| 175 | workflowName: input.name, |
| 176 | } |
| 177 | } |
| 178 | throw new Error('One of script, name, or scriptPath must be provided') |
| 179 | } |
no test coverage detected