How Namespaced Commands Create Directory Hierarchies in Conversion

Namespaced commands create directory hierarchies during conversion by splitting command names on colons, joining the segments with forward slashes, and passing the resulting path key to the file writer, which automatically generates nested folders that mirror the original namespace structure.

In the EveryInc/compound-engineering-plugin repository, converting Claude Code plugins to target formats requires handling command namespaces like workflows:plan. When targeting hierarchical formats such as Gemini, the conversion pipeline transforms these colon-separated identifiers into file-system paths, producing organized directory structures that reflect the logical command groupings.

The Namespace-to-Directory Pipeline

The conversion process follows a strict pipeline to transform flat command names into hierarchical directory trees. This logic lives exclusively in the Gemini converter, as other target formats deliberately flatten the namespace.

Parsing and Segmenting Namespaced Commands

The process begins in src/converters/claude-to-gemini.ts within the resolveCommandPath function. This utility splits the raw command name on the colon character (:) and normalizes each segment to ensure file-system compatibility.

// src/converters/claude-to-gemini.ts
function resolveCommandPath(name: string): string[] {
  // "workflows:plan" → ["workflows", "plan"]
  return name.split(":").map((segment) => normalizeName(segment))
}

For a command named workflows:plan, this returns an array ["workflows", "plan"]. If the command includes multiple namespaces, such as category:subcategory:task, the function returns a correspondingly deeper array.

Generating Hierarchical Path Keys

The convertCommand function receives these segments and joins them with forward slashes to create a pathKey. This key serves as the relative path for the output file.

// src/converters/claude-to-gemini.ts
function convertCommand(command: ClaudeCommand, usedNames: Set<string>): GeminiCommand {
  // Preserve namespace → "workflows:plan" becomes "workflows/plan"
  const commandPath = resolveCommandPath(command.name)   // ["workflows", "plan"]
  const pathKey = commandPath.join("/")                 // "workflows/plan"
  uniqueName(pathKey, usedNames)                       // ensure uniqueness

  const description = command.description ?? `Converted from Claude command ${command.name}`
  const transformedBody = transformContentForGemini(command.body.trim())
  const content = toToml(description, transformedBody)

  return { name: pathKey, content }                     // name contains the slash
}

The uniqueName function registers this key to prevent collisions when different namespaces contain identically named commands.

Writing Nested Directory Structures

The final step occurs in src/targets/gemini.ts within writeGeminiBundle. When the writer constructs the file path using path.join, the forward slashes embedded in command.name trigger automatic directory creation.

// src/targets/gemini.ts
await writeText(
  path.join(paths.commandsDir, `${command.name}.toml`), // e.g. ".../.gemini/commands/workflows/plan.toml"
  command.content + "\n"
);

Because command.name contains workflows/plan, the operating system creates the workflows directory and places plan.toml inside it, resulting in a structure like:


output/.gemini/
├─ commands/
│  ├─ workflows/
│  │  └─ plan.toml
│  └─ another/
│     └─ task.toml

Contrast with Flat-Structure Targets

Other target writers—specifically Droid, Cursor, Codex, and Pi—do not preserve hierarchical namespaces. Instead, they use flattenCommandName to strip the namespace prefix, ensuring compatibility with their flat file layouts.

In src/converters/claude-to-droid.ts, the implementation extracts only the segment after the final colon:

// src/converters/claude-to-droid.ts
function flattenCommandName(name: string): string {
  const colonIndex = name.lastIndexOf(":")
  const base = colonIndex >= 0 ? name.slice(colonIndex + 1) : name
  return normalizeName(base) // "workflows:plan" → "plan"
}

The Droid writer then writes plan.md directly under the commands directory, losing the workflows hierarchy. Similar logic exists in src/converters/claude-to-cursor.ts for Cursor-compatible output.

Summary

  • Namespace parsing in resolveCommandPath splits command names on colons and normalizes each segment.
  • Path generation joins segments with forward slashes to create file-system-compatible keys like workflows/plan.
  • Automatic directory creation occurs in writeGeminiBundle when path.join processes keys containing slashes, creating nested folders under the .gemini/commands directory.
  • Format-specific behavior means only Gemini retains hierarchies; other targets flatten namespaces using flattenCommandName to match their flat file requirements.

Frequently Asked Questions

How does the converter handle duplicate command names across different namespaces?

The uniqueName utility registers each generated pathKey in a Set to track used names. If a collision occurs—for example, both workflows:plan and tasks:plan resolving to the same flattened key—the function disambiguates the names to ensure unique file outputs.

Why does only the Gemini target support hierarchical directories?

Gemini's specification supports nested command structures organized by category, making it the only target format that benefits from preserved namespaces. According to the source code, Droid, Cursor, Codex, and Pi implement flattenCommandName specifically because their formats expect flat command listings without subdirectories.

What happens if a command name contains multiple colons?

The resolveCommandPath function splits on all colon characters, creating arbitrarily deep nesting. A command named project:workflows:plan generates the path key project/workflows/plan, resulting in three levels of directory hierarchy under the output directory.

Where is the directory creation logic located?

The actual directory creation happens implicitly in src/targets/gemini.ts within the writeGeminiBundle function. When Node.js's path.join combines paths.commandsDir with a command.name containing forward slashes, and writeText attempts to write the file, the operating system automatically creates the necessary parent directories to accommodate the path.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →