Merge pull request #1473 from code-yeongyu/feature/task-global-storage
feat(tasks): migrate storage to global config dir with ULTRAWORK_TASK_LIST_ID support
This commit is contained in:
commit
ce7478cde7
File diff suppressed because it is too large
Load Diff
@ -1,5 +1,6 @@
|
||||
#!/usr/bin/env bun
|
||||
import * as z from "zod"
|
||||
import { zodToJsonSchema } from "zod-to-json-schema"
|
||||
import { OhMyOpenCodeConfigSchema } from "../src/config/schema"
|
||||
|
||||
const SCHEMA_OUTPUT_PATH = "assets/oh-my-opencode.schema.json"
|
||||
@ -7,9 +8,8 @@ const SCHEMA_OUTPUT_PATH = "assets/oh-my-opencode.schema.json"
|
||||
async function main() {
|
||||
console.log("Generating JSON Schema...")
|
||||
|
||||
const jsonSchema = z.toJSONSchema(OhMyOpenCodeConfigSchema, {
|
||||
io: "input",
|
||||
target: "draft-7",
|
||||
const jsonSchema = zodToJsonSchema(OhMyOpenCodeConfigSchema, {
|
||||
target: "draft7",
|
||||
})
|
||||
|
||||
const finalSchema = {
|
||||
|
||||
@ -368,8 +368,10 @@ export const TmuxConfigSchema = z.object({
|
||||
})
|
||||
|
||||
export const SisyphusTasksConfigSchema = z.object({
|
||||
/** Storage path for tasks (default: .sisyphus/tasks) */
|
||||
storage_path: z.string().default(".sisyphus/tasks"),
|
||||
/** Absolute or relative storage path override. When set, bypasses global config dir. */
|
||||
storage_path: z.string().optional(),
|
||||
/** Force task list ID (alternative to env ULTRAWORK_TASK_LIST_ID) */
|
||||
task_list_id: z.string().optional(),
|
||||
/** Enable Claude Code path compatibility mode */
|
||||
claude_code_compat: z.boolean().default(false),
|
||||
})
|
||||
|
||||
@ -54,7 +54,26 @@ The task system includes a sync layer (`todo-sync.ts`) that automatically mirror
|
||||
|
||||
### getTaskDir(config)
|
||||
|
||||
Returns: `.sisyphus/tasks` (or custom path from config)
|
||||
Returns the task storage directory path.
|
||||
|
||||
**Default behavior (no config override):**
|
||||
Returns `~/.config/opencode/tasks/<task-list-id>/` where:
|
||||
- Task list ID is resolved via `resolveTaskListId()`
|
||||
|
||||
**With `storage_path` config:**
|
||||
- Absolute paths (starting with `/`) are returned as-is
|
||||
- Relative paths are joined with `process.cwd()`
|
||||
|
||||
### resolveTaskListId(config)
|
||||
|
||||
Resolves the task list ID for directory scoping.
|
||||
|
||||
**Priority order:**
|
||||
1. `ULTRAWORK_TASK_LIST_ID` environment variable
|
||||
2. `config.sisyphus?.tasks?.task_list_id` config option
|
||||
3. Sanitized `basename(process.cwd())` as fallback
|
||||
|
||||
**Sanitization:** Replaces non-alphanumeric characters (except `-` and `_`) with `-`
|
||||
|
||||
### readJsonSafe(filePath, schema)
|
||||
|
||||
|
||||
@ -1,26 +1,99 @@
|
||||
import { describe, test, expect, beforeEach, afterEach } from "bun:test"
|
||||
import { existsSync, mkdirSync, rmSync, writeFileSync } from "fs"
|
||||
import { join } from "path"
|
||||
import { join, basename } from "path"
|
||||
import { z } from "zod"
|
||||
import { getTaskDir, readJsonSafe, writeJsonAtomic, acquireLock, generateTaskId, listTaskFiles } from "./storage"
|
||||
import { getOpenCodeConfigDir } from "../../shared/opencode-config-dir"
|
||||
import {
|
||||
getTaskDir,
|
||||
readJsonSafe,
|
||||
writeJsonAtomic,
|
||||
acquireLock,
|
||||
generateTaskId,
|
||||
listTaskFiles,
|
||||
resolveTaskListId,
|
||||
sanitizePathSegment,
|
||||
} from "./storage"
|
||||
import type { OhMyOpenCodeConfig } from "../../config/schema"
|
||||
|
||||
const TEST_DIR = ".test-claude-tasks"
|
||||
const TEST_DIR_ABS = join(process.cwd(), TEST_DIR)
|
||||
|
||||
describe("getTaskDir", () => {
|
||||
test("returns correct path for default config", () => {
|
||||
const originalTaskListId = process.env.ULTRAWORK_TASK_LIST_ID
|
||||
|
||||
beforeEach(() => {
|
||||
if (originalTaskListId === undefined) {
|
||||
delete process.env.ULTRAWORK_TASK_LIST_ID
|
||||
} else {
|
||||
process.env.ULTRAWORK_TASK_LIST_ID = originalTaskListId
|
||||
}
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
if (originalTaskListId === undefined) {
|
||||
delete process.env.ULTRAWORK_TASK_LIST_ID
|
||||
} else {
|
||||
process.env.ULTRAWORK_TASK_LIST_ID = originalTaskListId
|
||||
}
|
||||
})
|
||||
|
||||
test("returns global config path for default config", () => {
|
||||
//#given
|
||||
const config: Partial<OhMyOpenCodeConfig> = {}
|
||||
const configDir = getOpenCodeConfigDir({ binary: "opencode" })
|
||||
const expectedListId = sanitizePathSegment(basename(process.cwd()))
|
||||
|
||||
//#when
|
||||
const result = getTaskDir(config)
|
||||
|
||||
//#then
|
||||
expect(result).toBe(join(process.cwd(), ".sisyphus/tasks"))
|
||||
expect(result).toBe(join(configDir, "tasks", expectedListId))
|
||||
})
|
||||
|
||||
test("returns correct path with custom storage_path", () => {
|
||||
test("respects ULTRAWORK_TASK_LIST_ID env var", () => {
|
||||
//#given
|
||||
process.env.ULTRAWORK_TASK_LIST_ID = "custom list/id"
|
||||
const configDir = getOpenCodeConfigDir({ binary: "opencode" })
|
||||
|
||||
//#when
|
||||
const result = getTaskDir()
|
||||
|
||||
//#then
|
||||
expect(result).toBe(join(configDir, "tasks", "custom-list-id"))
|
||||
})
|
||||
|
||||
test("falls back to sanitized cwd basename when env var not set", () => {
|
||||
//#given
|
||||
delete process.env.ULTRAWORK_TASK_LIST_ID
|
||||
const configDir = getOpenCodeConfigDir({ binary: "opencode" })
|
||||
const expectedListId = sanitizePathSegment(basename(process.cwd()))
|
||||
|
||||
//#when
|
||||
const result = getTaskDir()
|
||||
|
||||
//#then
|
||||
expect(result).toBe(join(configDir, "tasks", expectedListId))
|
||||
})
|
||||
|
||||
test("returns absolute storage_path without joining cwd", () => {
|
||||
//#given
|
||||
const config: Partial<OhMyOpenCodeConfig> = {
|
||||
sisyphus: {
|
||||
tasks: {
|
||||
storage_path: "/tmp/custom-task-path",
|
||||
claude_code_compat: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
//#when
|
||||
const result = getTaskDir(config)
|
||||
|
||||
//#then
|
||||
expect(result).toBe("/tmp/custom-task-path")
|
||||
})
|
||||
|
||||
test("joins relative storage_path with cwd", () => {
|
||||
//#given
|
||||
const config: Partial<OhMyOpenCodeConfig> = {
|
||||
sisyphus: {
|
||||
@ -37,13 +110,59 @@ describe("getTaskDir", () => {
|
||||
//#then
|
||||
expect(result).toBe(join(process.cwd(), ".custom/tasks"))
|
||||
})
|
||||
})
|
||||
|
||||
describe("resolveTaskListId", () => {
|
||||
const originalTaskListId = process.env.ULTRAWORK_TASK_LIST_ID
|
||||
|
||||
beforeEach(() => {
|
||||
if (originalTaskListId === undefined) {
|
||||
delete process.env.ULTRAWORK_TASK_LIST_ID
|
||||
} else {
|
||||
process.env.ULTRAWORK_TASK_LIST_ID = originalTaskListId
|
||||
}
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
if (originalTaskListId === undefined) {
|
||||
delete process.env.ULTRAWORK_TASK_LIST_ID
|
||||
} else {
|
||||
process.env.ULTRAWORK_TASK_LIST_ID = originalTaskListId
|
||||
}
|
||||
})
|
||||
|
||||
test("returns env var when set", () => {
|
||||
//#given
|
||||
process.env.ULTRAWORK_TASK_LIST_ID = "custom-list"
|
||||
|
||||
test("returns correct path with default config parameter", () => {
|
||||
//#when
|
||||
const result = getTaskDir()
|
||||
const result = resolveTaskListId()
|
||||
|
||||
//#then
|
||||
expect(result).toBe(join(process.cwd(), ".sisyphus/tasks"))
|
||||
expect(result).toBe("custom-list")
|
||||
})
|
||||
|
||||
test("sanitizes special characters", () => {
|
||||
//#given
|
||||
process.env.ULTRAWORK_TASK_LIST_ID = "custom list/id"
|
||||
|
||||
//#when
|
||||
const result = resolveTaskListId()
|
||||
|
||||
//#then
|
||||
expect(result).toBe("custom-list-id")
|
||||
})
|
||||
|
||||
test("returns sanitized cwd basename when env var not set", () => {
|
||||
//#given
|
||||
delete process.env.ULTRAWORK_TASK_LIST_ID
|
||||
const expected = sanitizePathSegment(basename(process.cwd()))
|
||||
|
||||
//#when
|
||||
const result = resolveTaskListId()
|
||||
|
||||
//#then
|
||||
expect(result).toBe(expected)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -1,13 +1,35 @@
|
||||
import { join, dirname } from "path"
|
||||
import { join, dirname, basename, isAbsolute } from "path"
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync, unlinkSync, readdirSync } from "fs"
|
||||
import { randomUUID } from "crypto"
|
||||
import { getOpenCodeConfigDir } from "../../shared/opencode-config-dir"
|
||||
import type { z } from "zod"
|
||||
import type { OhMyOpenCodeConfig } from "../../config/schema"
|
||||
|
||||
export function getTaskDir(config: Partial<OhMyOpenCodeConfig> = {}): string {
|
||||
const tasksConfig = config.sisyphus?.tasks
|
||||
const storagePath = tasksConfig?.storage_path ?? ".sisyphus/tasks"
|
||||
return join(process.cwd(), storagePath)
|
||||
const storagePath = tasksConfig?.storage_path
|
||||
|
||||
if (storagePath) {
|
||||
return isAbsolute(storagePath) ? storagePath : join(process.cwd(), storagePath)
|
||||
}
|
||||
|
||||
const configDir = getOpenCodeConfigDir({ binary: "opencode" })
|
||||
const listId = resolveTaskListId(config)
|
||||
return join(configDir, "tasks", listId)
|
||||
}
|
||||
|
||||
export function sanitizePathSegment(value: string): string {
|
||||
return value.replace(/[^a-zA-Z0-9_-]/g, "-") || "default"
|
||||
}
|
||||
|
||||
export function resolveTaskListId(config: Partial<OhMyOpenCodeConfig> = {}): string {
|
||||
const envId = process.env.ULTRAWORK_TASK_LIST_ID?.trim()
|
||||
if (envId) return sanitizePathSegment(envId)
|
||||
|
||||
const configId = config.sisyphus?.tasks?.task_list_id?.trim()
|
||||
if (configId) return sanitizePathSegment(configId)
|
||||
|
||||
return sanitizePathSegment(basename(process.cwd()))
|
||||
}
|
||||
|
||||
export function ensureDir(dirPath: string): void {
|
||||
|
||||
@ -4,7 +4,7 @@ import { existsSync, readdirSync } from "fs"
|
||||
import type { OhMyOpenCodeConfig } from "../../config/schema"
|
||||
import type { TaskObject, TaskStatus } from "./types"
|
||||
import { TaskObjectSchema } from "./types"
|
||||
import { readJsonSafe } from "../../features/claude-tasks/storage"
|
||||
import { readJsonSafe, getTaskDir } from "../../features/claude-tasks/storage"
|
||||
|
||||
interface TaskSummary {
|
||||
id: string
|
||||
@ -23,9 +23,7 @@ For each task's blockedBy field, filters to only include unresolved (non-complet
|
||||
Returns summary format: id, subject, status, owner, blockedBy (not full description).`,
|
||||
args: {},
|
||||
execute: async (): Promise<string> => {
|
||||
const tasksConfig = config.sisyphus?.tasks
|
||||
const storagePath = tasksConfig?.storage_path ?? ".sisyphus/tasks"
|
||||
const taskDir = storagePath.startsWith("/") ? storagePath : join(process.cwd(), storagePath)
|
||||
const taskDir = getTaskDir(config)
|
||||
|
||||
if (!existsSync(taskDir)) {
|
||||
return JSON.stringify({ tasks: [] })
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user