fix(installer): OpenCode integration: replace `name` frontmatter with `mode: all` and update directory names (#1764)

* fix(opencode): use mode: all in agent template, remove name frontmatter, fix directory names

- Replace name: '{{name}}' with mode: all in opencode-agent.md
  mode: all enables both Tab-key agent switching in the TUI and @subagent
  invocation via the Task tool (mode: primary blocked subagent use)
- Remove name: '{{name}}' from opencode-task/tool/workflow/workflow-yaml templates
  OpenCode derives command name from filename, not from a name frontmatter field;
  the bare {{name}} value was overriding the bmad- prefixed filename causing
  name collisions with built-in OpenCode commands (fixes #1762)
- Fix deprecated singular directory names in platform-codes.yaml:
  .opencode/agent -> .opencode/agents, .opencode/command -> .opencode/commands
- Add legacy_targets migration: cleanup() now removes stale bmad-* files from
  old singular directories on reinstall so existing users don't get duplicates
- Fix removeEmptyParents to continue walking up to parent when starting dir is
  already absent instead of breaking early

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(opencode): address code review findings for cleanup and schema docs

- Add project boundary guard to removeEmptyParents() using path.resolve
  and startsWith check to prevent traversal outside projectDir (Augment)
- Fix JSDoc: "Recursively remove" -> "Walk up ancestor directories"
- Add user-visible migration log message when processing legacy_targets
- Document legacy_targets field in Installer Config Schema comment block
  in platform-codes.yaml (CodeRabbit + Augment)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(opencode): improve removeEmptyParents error handling and loop clarity

- Distinguish recoverable errors (ENOTEMPTY, ENOENT) from fatal errors in
  removeEmptyParents() catch block — skip level and continue upward on
  TOCTOU races or concurrent removal, break only on fatal errors (EACCES)
- Add comment clarifying loop invariant for missing-path continue branch

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Brian <bmadcode@gmail.com>
This commit is contained in:
Davor Racic 2026-02-25 18:12:05 +01:00 committed by GitHub
parent 72a9325a40
commit 6bfc937bd3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 35 additions and 12 deletions

View File

@ -453,6 +453,15 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
* @param {string} projectDir - Project directory
*/
async cleanup(projectDir, options = {}) {
// Migrate legacy target directories (e.g. .opencode/agent → .opencode/agents)
if (this.installerConfig?.legacy_targets) {
if (!options.silent) await prompts.log.message(' Migrating legacy directories...');
for (const legacyDir of this.installerConfig.legacy_targets) {
await this.cleanupTarget(projectDir, legacyDir, options);
await this.removeEmptyParents(projectDir, legacyDir);
}
}
// Clean all target directories
if (this.installerConfig?.targets) {
const parentDirs = new Set();
@ -532,24 +541,37 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
}
}
/**
* Recursively remove empty directories walking up from dir toward projectDir
* Walk up ancestor directories from relativeDir toward projectDir, removing each if empty
* Stops at projectDir boundary never removes projectDir itself
* @param {string} projectDir - Project root (boundary)
* @param {string} relativeDir - Relative directory to start from
*/
async removeEmptyParents(projectDir, relativeDir) {
const resolvedProject = path.resolve(projectDir);
let current = relativeDir;
let last = null;
while (current && current !== '.' && current !== last) {
last = current;
const fullPath = path.join(projectDir, current);
const fullPath = path.resolve(projectDir, current);
// Boundary guard: never traverse outside projectDir
if (!fullPath.startsWith(resolvedProject + path.sep) && fullPath !== resolvedProject) break;
try {
if (!(await fs.pathExists(fullPath))) break;
if (!(await fs.pathExists(fullPath))) {
// Dir already gone — advance current; last is reset at top of next iteration
current = path.dirname(current);
continue;
}
const remaining = await fs.readdir(fullPath);
if (remaining.length > 0) break;
await fs.rmdir(fullPath);
} catch {
break;
} catch (error) {
// ENOTEMPTY: TOCTOU race (file added between readdir and rmdir) — skip level, continue upward
// ENOENT: dir removed by another process between pathExists and rmdir — skip level, continue upward
if (error.code === 'ENOTEMPTY' || error.code === 'ENOENT') {
current = path.dirname(current);
continue;
}
break; // fatal error (e.g. EACCES) — stop upward walk
}
current = path.dirname(current);
}

View File

@ -131,11 +131,14 @@ platforms:
category: ide
description: "OpenCode terminal coding assistant"
installer:
legacy_targets:
- .opencode/agent
- .opencode/command
targets:
- target_dir: .opencode/agent
- target_dir: .opencode/agents
template_type: opencode
artifact_types: [agents]
- target_dir: .opencode/command
- target_dir: .opencode/commands
template_type: opencode
artifact_types: [workflows, tasks, tools]
@ -191,6 +194,8 @@ platforms:
# template_type: string # Default template type to use
# header_template: string (optional) # Override for header/frontmatter template
# body_template: string (optional) # Override for body/content template
# legacy_targets: array (optional) # Old target dirs to clean up on reinstall (migration)
# - string # Relative path, e.g. .opencode/agent
# targets: array (optional) # For multi-target installations
# - target_dir: string
# template_type: string

View File

@ -1,5 +1,5 @@
---
name: '{{name}}'
mode: all
description: '{{description}}'
---

View File

@ -1,5 +1,4 @@
---
name: '{{name}}'
description: '{{description}}'
---

View File

@ -1,5 +1,4 @@
---
name: '{{name}}'
description: '{{description}}'
---

View File

@ -1,5 +1,4 @@
---
name: '{{name}}'
description: '{{description}}'
---

View File

@ -1,5 +1,4 @@
---
name: '{{name}}'
description: '{{description}}'
---