feat(workspace): migrate legacy sisyphus state to omo
This commit is contained in:
@@ -4,6 +4,7 @@ import { createPluginModule } from "./testing/create-plugin-module"
|
|||||||
const mockInitConfigContext = mock(() => {})
|
const mockInitConfigContext = mock(() => {})
|
||||||
const mockInjectServerAuthIntoClient = mock(() => {})
|
const mockInjectServerAuthIntoClient = mock(() => {})
|
||||||
const mockLogLegacyPluginStartupWarning = mock(() => {})
|
const mockLogLegacyPluginStartupWarning = mock(() => {})
|
||||||
|
const mockMigrateLegacyWorkspaceDirectory = mock(() => ({ migrated: false, skipped: [] }))
|
||||||
const mockLoadPluginConfig = mock(() => ({}))
|
const mockLoadPluginConfig = mock(() => ({}))
|
||||||
const mockIsTmuxIntegrationEnabled = mock(() => false)
|
const mockIsTmuxIntegrationEnabled = mock(() => false)
|
||||||
const mockCreateRuntimeTmuxConfig = mock(() => ({
|
const mockCreateRuntimeTmuxConfig = mock(() => ({
|
||||||
@@ -38,6 +39,7 @@ function createTestPluginModule(): ReturnType<typeof createPluginModule> {
|
|||||||
initConfigContext: mockInitConfigContext,
|
initConfigContext: mockInitConfigContext,
|
||||||
injectServerAuthIntoClient: mockInjectServerAuthIntoClient,
|
injectServerAuthIntoClient: mockInjectServerAuthIntoClient,
|
||||||
logLegacyPluginStartupWarning: mockLogLegacyPluginStartupWarning,
|
logLegacyPluginStartupWarning: mockLogLegacyPluginStartupWarning,
|
||||||
|
migrateLegacyWorkspaceDirectory: mockMigrateLegacyWorkspaceDirectory,
|
||||||
loadPluginConfig: mockLoadPluginConfig as never,
|
loadPluginConfig: mockLoadPluginConfig as never,
|
||||||
isTmuxIntegrationEnabled: mockIsTmuxIntegrationEnabled as never,
|
isTmuxIntegrationEnabled: mockIsTmuxIntegrationEnabled as never,
|
||||||
createRuntimeTmuxConfig: mockCreateRuntimeTmuxConfig as never,
|
createRuntimeTmuxConfig: mockCreateRuntimeTmuxConfig as never,
|
||||||
@@ -67,6 +69,7 @@ describe("oh-my-openagent telemetry isolation", () => {
|
|||||||
mockInitConfigContext.mockClear()
|
mockInitConfigContext.mockClear()
|
||||||
mockInjectServerAuthIntoClient.mockClear()
|
mockInjectServerAuthIntoClient.mockClear()
|
||||||
mockLogLegacyPluginStartupWarning.mockClear()
|
mockLogLegacyPluginStartupWarning.mockClear()
|
||||||
|
mockMigrateLegacyWorkspaceDirectory.mockClear()
|
||||||
mockLoadPluginConfig.mockClear()
|
mockLoadPluginConfig.mockClear()
|
||||||
mockIsTmuxIntegrationEnabled.mockClear()
|
mockIsTmuxIntegrationEnabled.mockClear()
|
||||||
mockCreateRuntimeTmuxConfig.mockClear()
|
mockCreateRuntimeTmuxConfig.mockClear()
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ const mockDetectExternalSkillPlugin = mock(() => ({ detected: false, pluginName:
|
|||||||
const mockGetSkillPluginConflictWarning = mock(() => "")
|
const mockGetSkillPluginConflictWarning = mock(() => "")
|
||||||
const mockInjectServerAuthIntoClient = mock(() => {})
|
const mockInjectServerAuthIntoClient = mock(() => {})
|
||||||
const mockLogLegacyPluginStartupWarning = mock(() => {})
|
const mockLogLegacyPluginStartupWarning = mock(() => {})
|
||||||
|
const mockMigrateLegacyWorkspaceDirectory = mock(() => ({ migrated: false, skipped: [] }))
|
||||||
const mockLoadPluginConfig = mock(() => ({}))
|
const mockLoadPluginConfig = mock(() => ({}))
|
||||||
const mockIsTmuxIntegrationEnabled = mock(
|
const mockIsTmuxIntegrationEnabled = mock(
|
||||||
(pluginConfig: { tmux?: { enabled?: boolean } | undefined }) => pluginConfig.tmux?.enabled ?? false,
|
(pluginConfig: { tmux?: { enabled?: boolean } | undefined }) => pluginConfig.tmux?.enabled ?? false,
|
||||||
@@ -57,6 +58,7 @@ function createTestPluginModule(): ReturnType<typeof createPluginModule> {
|
|||||||
getSkillPluginConflictWarning: mockGetSkillPluginConflictWarning,
|
getSkillPluginConflictWarning: mockGetSkillPluginConflictWarning,
|
||||||
injectServerAuthIntoClient: mockInjectServerAuthIntoClient,
|
injectServerAuthIntoClient: mockInjectServerAuthIntoClient,
|
||||||
logLegacyPluginStartupWarning: mockLogLegacyPluginStartupWarning,
|
logLegacyPluginStartupWarning: mockLogLegacyPluginStartupWarning,
|
||||||
|
migrateLegacyWorkspaceDirectory: mockMigrateLegacyWorkspaceDirectory,
|
||||||
loadPluginConfig: mockLoadPluginConfig as never,
|
loadPluginConfig: mockLoadPluginConfig as never,
|
||||||
isTmuxIntegrationEnabled: mockIsTmuxIntegrationEnabled as never,
|
isTmuxIntegrationEnabled: mockIsTmuxIntegrationEnabled as never,
|
||||||
createRuntimeTmuxConfig: mockCreateRuntimeTmuxConfig as never,
|
createRuntimeTmuxConfig: mockCreateRuntimeTmuxConfig as never,
|
||||||
@@ -81,6 +83,7 @@ describe("oh-my-openagent plugin module", () => {
|
|||||||
mockGetSkillPluginConflictWarning.mockClear()
|
mockGetSkillPluginConflictWarning.mockClear()
|
||||||
mockInjectServerAuthIntoClient.mockClear()
|
mockInjectServerAuthIntoClient.mockClear()
|
||||||
mockLogLegacyPluginStartupWarning.mockClear()
|
mockLogLegacyPluginStartupWarning.mockClear()
|
||||||
|
mockMigrateLegacyWorkspaceDirectory.mockClear()
|
||||||
mockLoadPluginConfig.mockClear()
|
mockLoadPluginConfig.mockClear()
|
||||||
mockIsTmuxIntegrationEnabled.mockClear()
|
mockIsTmuxIntegrationEnabled.mockClear()
|
||||||
mockCreateRuntimeTmuxConfig.mockClear()
|
mockCreateRuntimeTmuxConfig.mockClear()
|
||||||
@@ -134,6 +137,25 @@ describe("oh-my-openagent plugin module", () => {
|
|||||||
expect(mockInitializeOpenClaw).not.toHaveBeenCalled()
|
expect(mockInitializeOpenClaw).not.toHaveBeenCalled()
|
||||||
}, { timeout: 15000 })
|
}, { timeout: 15000 })
|
||||||
|
|
||||||
|
it("migrates legacy workspace state during plugin bootstrap", async () => {
|
||||||
|
// given
|
||||||
|
const directory = "/tmp/project"
|
||||||
|
mockLoadPluginConfig.mockReturnValue({})
|
||||||
|
|
||||||
|
// when
|
||||||
|
await pluginModule.server({
|
||||||
|
directory,
|
||||||
|
client: {},
|
||||||
|
} as Parameters<typeof pluginModule.server>[0])
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(mockMigrateLegacyWorkspaceDirectory).toHaveBeenCalledTimes(1)
|
||||||
|
expect(mockMigrateLegacyWorkspaceDirectory).toHaveBeenCalledWith(directory)
|
||||||
|
expect(mockMigrateLegacyWorkspaceDirectory.mock.invocationCallOrder[0]).toBeLessThan(
|
||||||
|
mockLoadPluginConfig.mock.invocationCallOrder[0] ?? Number.MAX_SAFE_INTEGER,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
it("exports a V1 PluginModule shape with id and server", () => {
|
it("exports a V1 PluginModule shape with id and server", () => {
|
||||||
// given the plugin module is loaded
|
// given the plugin module is loaded
|
||||||
// when inspecting the default export
|
// when inspecting the default export
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ export * from "./plugin-command-discovery"
|
|||||||
export { SessionCategoryRegistry } from "./session-category-registry"
|
export { SessionCategoryRegistry } from "./session-category-registry"
|
||||||
export * from "./plugin-identity"
|
export * from "./plugin-identity"
|
||||||
export * from "./log-legacy-plugin-startup-warning"
|
export * from "./log-legacy-plugin-startup-warning"
|
||||||
|
export * from "./legacy-workspace-migration"
|
||||||
export * from "./task-system-enabled"
|
export * from "./task-system-enabled"
|
||||||
export * from "./parse-tools-config"
|
export * from "./parse-tools-config"
|
||||||
export { parseModelString } from "./model-string-parser"
|
export { parseModelString } from "./model-string-parser"
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test"
|
||||||
|
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"
|
||||||
|
import { tmpdir } from "node:os"
|
||||||
|
import { join } from "node:path"
|
||||||
|
import { migrateLegacyWorkspaceDirectory } from "./legacy-workspace-migration"
|
||||||
|
|
||||||
|
describe("migrateLegacyWorkspaceDirectory", () => {
|
||||||
|
let testDirectory = ""
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
testDirectory = join(tmpdir(), `omo-workspace-migration-${Date.now()}-${Math.random().toString(36).slice(2)}`)
|
||||||
|
mkdirSync(testDirectory, { recursive: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
rmSync(testDirectory, { recursive: true, force: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
test("#given legacy workspace with nested state and no target #when migrating #then copies the tree to .omo", () => {
|
||||||
|
// given
|
||||||
|
const legacyPlanPath = join(testDirectory, ".sisyphus", "plans", "work.md")
|
||||||
|
const legacyNotepadDirectory = join(testDirectory, ".sisyphus", "notepads", "work")
|
||||||
|
const legacyNotepadPath = join(legacyNotepadDirectory, "notes.md")
|
||||||
|
mkdirSync(legacyNotepadDirectory, { recursive: true })
|
||||||
|
mkdirSync(join(testDirectory, ".sisyphus", "plans"), { recursive: true })
|
||||||
|
writeFileSync(legacyPlanPath, "# Plan", "utf-8")
|
||||||
|
writeFileSync(legacyNotepadPath, "note", "utf-8")
|
||||||
|
|
||||||
|
// when
|
||||||
|
const result = migrateLegacyWorkspaceDirectory(testDirectory)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result.migrated).toBe(true)
|
||||||
|
expect(readFileSync(join(testDirectory, ".omo", "plans", "work.md"), "utf-8")).toBe("# Plan")
|
||||||
|
expect(readFileSync(join(testDirectory, ".omo", "notepads", "work", "notes.md"), "utf-8")).toBe("note")
|
||||||
|
expect(existsSync(join(testDirectory, ".sisyphus", "plans", "work.md"))).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("#given target file already exists #when migrating #then keeps the target content", () => {
|
||||||
|
// given
|
||||||
|
const legacyPlanPath = join(testDirectory, ".sisyphus", "plans", "work.md")
|
||||||
|
const targetPlanPath = join(testDirectory, ".omo", "plans", "work.md")
|
||||||
|
mkdirSync(join(testDirectory, ".sisyphus", "plans"), { recursive: true })
|
||||||
|
mkdirSync(join(testDirectory, ".omo", "plans"), { recursive: true })
|
||||||
|
writeFileSync(legacyPlanPath, "legacy", "utf-8")
|
||||||
|
writeFileSync(targetPlanPath, "target", "utf-8")
|
||||||
|
|
||||||
|
// when
|
||||||
|
const result = migrateLegacyWorkspaceDirectory(testDirectory)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result.migrated).toBe(false)
|
||||||
|
expect(result.skipped).toContain(join(".omo", "plans", "work.md"))
|
||||||
|
expect(readFileSync(targetPlanPath, "utf-8")).toBe("target")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("#given target has other files #when migrating #then copies only missing legacy files", () => {
|
||||||
|
// given
|
||||||
|
const legacyPlanPath = join(testDirectory, ".sisyphus", "plans", "work.md")
|
||||||
|
const targetNotepadPath = join(testDirectory, ".omo", "notepads", "work", "notes.md")
|
||||||
|
mkdirSync(join(testDirectory, ".sisyphus", "plans"), { recursive: true })
|
||||||
|
mkdirSync(join(testDirectory, ".omo", "notepads", "work"), { recursive: true })
|
||||||
|
writeFileSync(legacyPlanPath, "legacy plan", "utf-8")
|
||||||
|
writeFileSync(targetNotepadPath, "existing note", "utf-8")
|
||||||
|
|
||||||
|
// when
|
||||||
|
const result = migrateLegacyWorkspaceDirectory(testDirectory)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result.migrated).toBe(true)
|
||||||
|
expect(readFileSync(join(testDirectory, ".omo", "plans", "work.md"), "utf-8")).toBe("legacy plan")
|
||||||
|
expect(readFileSync(targetNotepadPath, "utf-8")).toBe("existing note")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("#given no legacy workspace #when migrating #then reports no migration", () => {
|
||||||
|
// when
|
||||||
|
const result = migrateLegacyWorkspaceDirectory(testDirectory)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result).toEqual({ migrated: false, skipped: [] })
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import { copyFileSync, existsSync, mkdirSync, readdirSync, statSync } from "node:fs"
|
||||||
|
import { dirname, join, relative } from "node:path"
|
||||||
|
|
||||||
|
import { log } from "./logger"
|
||||||
|
|
||||||
|
const LEGACY_WORKSPACE_DIR = ".sisyphus"
|
||||||
|
const WORKSPACE_DIR = ".omo"
|
||||||
|
|
||||||
|
export type LegacyWorkspaceMigrationResult = {
|
||||||
|
migrated: boolean
|
||||||
|
skipped: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyMissingEntries(legacyPath: string, targetPath: string, targetRoot: string, skipped: string[]): boolean {
|
||||||
|
const legacyStat = statSync(legacyPath)
|
||||||
|
|
||||||
|
if (existsSync(targetPath)) {
|
||||||
|
if (legacyStat.isDirectory() && statSync(targetPath).isDirectory()) {
|
||||||
|
let copiedChild = false
|
||||||
|
for (const entry of readdirSync(legacyPath)) {
|
||||||
|
copiedChild = copyMissingEntries(join(legacyPath, entry), join(targetPath, entry), targetRoot, skipped) || copiedChild
|
||||||
|
}
|
||||||
|
return copiedChild
|
||||||
|
}
|
||||||
|
|
||||||
|
skipped.push(join(WORKSPACE_DIR, relative(targetRoot, targetPath)))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (legacyStat.isDirectory()) {
|
||||||
|
mkdirSync(targetPath, { recursive: true })
|
||||||
|
let copiedChild = false
|
||||||
|
for (const entry of readdirSync(legacyPath)) {
|
||||||
|
copiedChild = copyMissingEntries(join(legacyPath, entry), join(targetPath, entry), targetRoot, skipped) || copiedChild
|
||||||
|
}
|
||||||
|
return copiedChild
|
||||||
|
}
|
||||||
|
|
||||||
|
mkdirSync(dirname(targetPath), { recursive: true })
|
||||||
|
copyFileSync(legacyPath, targetPath)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
export function migrateLegacyWorkspaceDirectory(directory: string): LegacyWorkspaceMigrationResult {
|
||||||
|
const legacyDirectory = join(directory, LEGACY_WORKSPACE_DIR)
|
||||||
|
if (!existsSync(legacyDirectory)) {
|
||||||
|
return { migrated: false, skipped: [] }
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetDirectory = join(directory, WORKSPACE_DIR)
|
||||||
|
const skipped: string[] = []
|
||||||
|
|
||||||
|
try {
|
||||||
|
const migrated = copyMissingEntries(legacyDirectory, targetDirectory, targetDirectory, skipped)
|
||||||
|
if (migrated || skipped.length > 0) {
|
||||||
|
log("[legacy-workspace-migration] Checked legacy workspace directory", {
|
||||||
|
legacyDirectory,
|
||||||
|
targetDirectory,
|
||||||
|
migrated,
|
||||||
|
skipped,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return { migrated, skipped }
|
||||||
|
} catch (error) {
|
||||||
|
log("[legacy-workspace-migration] Failed to migrate legacy workspace directory", {
|
||||||
|
legacyDirectory,
|
||||||
|
targetDirectory,
|
||||||
|
error,
|
||||||
|
})
|
||||||
|
return { migrated: false, skipped }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,6 +20,7 @@ import { detectExternalSkillPlugin, getSkillPluginConflictWarning } from "../sha
|
|||||||
import { createFirstMessageVariantGate } from "../shared/first-message-variant"
|
import { createFirstMessageVariantGate } from "../shared/first-message-variant"
|
||||||
import { log } from "../shared/logger"
|
import { log } from "../shared/logger"
|
||||||
import { logLegacyPluginStartupWarning } from "../shared/log-legacy-plugin-startup-warning"
|
import { logLegacyPluginStartupWarning } from "../shared/log-legacy-plugin-startup-warning"
|
||||||
|
import { migrateLegacyWorkspaceDirectory } from "../shared/legacy-workspace-migration"
|
||||||
import { injectServerAuthIntoClient } from "../shared/opencode-server-auth"
|
import { injectServerAuthIntoClient } from "../shared/opencode-server-auth"
|
||||||
import { startBackgroundCheck as startTmuxCheck } from "../tools/interactive-bash"
|
import { startBackgroundCheck as startTmuxCheck } from "../tools/interactive-bash"
|
||||||
|
|
||||||
@@ -33,6 +34,7 @@ export type PluginModuleDeps = {
|
|||||||
setAgentSortOrder: typeof setAgentSortOrder
|
setAgentSortOrder: typeof setAgentSortOrder
|
||||||
log: typeof log
|
log: typeof log
|
||||||
logLegacyPluginStartupWarning: typeof logLegacyPluginStartupWarning
|
logLegacyPluginStartupWarning: typeof logLegacyPluginStartupWarning
|
||||||
|
migrateLegacyWorkspaceDirectory: typeof migrateLegacyWorkspaceDirectory
|
||||||
detectExternalSkillPlugin: typeof detectExternalSkillPlugin
|
detectExternalSkillPlugin: typeof detectExternalSkillPlugin
|
||||||
getSkillPluginConflictWarning: typeof getSkillPluginConflictWarning
|
getSkillPluginConflictWarning: typeof getSkillPluginConflictWarning
|
||||||
injectServerAuthIntoClient: typeof injectServerAuthIntoClient
|
injectServerAuthIntoClient: typeof injectServerAuthIntoClient
|
||||||
@@ -55,6 +57,7 @@ const defaultPluginModuleDeps: PluginModuleDeps = {
|
|||||||
setAgentSortOrder,
|
setAgentSortOrder,
|
||||||
log,
|
log,
|
||||||
logLegacyPluginStartupWarning,
|
logLegacyPluginStartupWarning,
|
||||||
|
migrateLegacyWorkspaceDirectory,
|
||||||
detectExternalSkillPlugin,
|
detectExternalSkillPlugin,
|
||||||
getSkillPluginConflictWarning,
|
getSkillPluginConflictWarning,
|
||||||
injectServerAuthIntoClient,
|
injectServerAuthIntoClient,
|
||||||
@@ -80,6 +83,7 @@ export function createPluginModule(overrides: Partial<PluginModuleDeps> = {}): P
|
|||||||
directory: input.directory,
|
directory: input.directory,
|
||||||
})
|
})
|
||||||
deps.logLegacyPluginStartupWarning()
|
deps.logLegacyPluginStartupWarning()
|
||||||
|
deps.migrateLegacyWorkspaceDirectory(input.directory)
|
||||||
|
|
||||||
const skillPluginCheck = deps.detectExternalSkillPlugin(input.directory)
|
const skillPluginCheck = deps.detectExternalSkillPlugin(input.directory)
|
||||||
if (skillPluginCheck.detected && skillPluginCheck.pluginName) {
|
if (skillPluginCheck.detected && skillPluginCheck.pluginName) {
|
||||||
|
|||||||
Reference in New Issue
Block a user