Add Factory Droid as a converter target (#174)

Adds a new 'droid' target to the converter that outputs Claude Code plugins
in Factory Droid's format:

- Commands flattened to ~/.factory/commands/ (strips namespace prefixes)
- Agents converted to droids in ~/.factory/droids/ with proper frontmatter
- Skills copied to ~/.factory/skills/
- Content transforms: Task calls, slash commands, and @agent references
  adapted to Droid conventions

This resolves the manual workaround described in issue #31 by automating
the conversion from Claude Code plugin format to Factory Droid's expected
directory structure.

Includes 13 tests covering converter logic and file writer behavior.

Co-authored-by: adamprime <adamprime@hey.com>
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
This commit is contained in:
Adam Tervort
2026-02-11 11:48:13 -06:00
committed by GitHub
parent e8f3bbcb35
commit 4ab08dce78
8 changed files with 648 additions and 10 deletions

View File

@@ -22,7 +22,7 @@ export default defineCommand({
to: {
type: "string",
default: "opencode",
description: "Target format (opencode | codex)",
description: "Target format (opencode | codex | droid)",
},
output: {
type: "string",
@@ -80,7 +80,7 @@ export default defineCommand({
permissions: permissions as PermissionMode,
}
const primaryOutputRoot = targetName === "codex" && codexHome ? codexHome : outputRoot
const primaryOutputRoot = resolveTargetOutputRoot(targetName, outputRoot, codexHome)
const bundle = target.convert(plugin, options)
if (!bundle) {
throw new Error(`Target ${targetName} did not return a bundle.`)
@@ -106,9 +106,7 @@ export default defineCommand({
console.warn(`Skipping ${extra}: no output returned.`)
continue
}
const extraRoot = extra === "codex" && codexHome
? codexHome
: path.join(outputRoot, extra)
const extraRoot = resolveTargetOutputRoot(extra, path.join(outputRoot, extra), codexHome)
await handler.write(extraRoot, extraBundle)
console.log(`Converted ${plugin.manifest.name} to ${extra} at ${extraRoot}`)
}
@@ -154,3 +152,9 @@ function resolveOutputRoot(value: unknown): string {
}
return process.cwd()
}
function resolveTargetOutputRoot(targetName: string, outputRoot: string, codexHome: string): string {
if (targetName === "codex") return codexHome
if (targetName === "droid") return path.join(os.homedir(), ".factory")
return outputRoot
}

View File

@@ -24,7 +24,7 @@ export default defineCommand({
to: {
type: "string",
default: "opencode",
description: "Target format (opencode | codex)",
description: "Target format (opencode | codex | droid)",
},
output: {
type: "string",
@@ -88,7 +88,7 @@ export default defineCommand({
if (!bundle) {
throw new Error(`Target ${targetName} did not return a bundle.`)
}
const primaryOutputRoot = targetName === "codex" && codexHome ? codexHome : outputRoot
const primaryOutputRoot = resolveTargetOutputRoot(targetName, outputRoot, codexHome)
await target.write(primaryOutputRoot, bundle)
console.log(`Installed ${plugin.manifest.name} to ${primaryOutputRoot}`)
@@ -109,9 +109,7 @@ export default defineCommand({
console.warn(`Skipping ${extra}: no output returned.`)
continue
}
const extraRoot = extra === "codex" && codexHome
? codexHome
: path.join(outputRoot, extra)
const extraRoot = resolveTargetOutputRoot(extra, path.join(outputRoot, extra), codexHome)
await handler.write(extraRoot, extraBundle)
console.log(`Installed ${plugin.manifest.name} to ${extraRoot}`)
}
@@ -180,6 +178,12 @@ function resolveOutputRoot(value: unknown): string {
return path.join(os.homedir(), ".config", "opencode")
}
function resolveTargetOutputRoot(targetName: string, outputRoot: string, codexHome: string): string {
if (targetName === "codex") return codexHome
if (targetName === "droid") return path.join(os.homedir(), ".factory")
return outputRoot
}
async function resolveGitHubPluginPath(pluginName: string): Promise<ResolvedPluginPath> {
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "compound-plugin-"))
const source = resolveGitHubSource()