Skip to content

Commit 6bfc937

Browse files
dracicclaudebmadcode
authored
fix(installer): OpenCode integration: replace name frontmatter with mode: all and update directory names (bmad-code-org#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 bmad-code-org#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>
1 parent 72a9325 commit 6bfc937

File tree

7 files changed

+35
-12
lines changed

7 files changed

+35
-12
lines changed

tools/cli/installers/lib/ide/_config-driven.js

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -453,6 +453,15 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
453453
* @param {string} projectDir - Project directory
454454
*/
455455
async cleanup(projectDir, options = {}) {
456+
// Migrate legacy target directories (e.g. .opencode/agent → .opencode/agents)
457+
if (this.installerConfig?.legacy_targets) {
458+
if (!options.silent) await prompts.log.message(' Migrating legacy directories...');
459+
for (const legacyDir of this.installerConfig.legacy_targets) {
460+
await this.cleanupTarget(projectDir, legacyDir, options);
461+
await this.removeEmptyParents(projectDir, legacyDir);
462+
}
463+
}
464+
456465
// Clean all target directories
457466
if (this.installerConfig?.targets) {
458467
const parentDirs = new Set();
@@ -532,24 +541,37 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
532541
}
533542
}
534543
/**
535-
* Recursively remove empty directories walking up from dir toward projectDir
544+
* Walk up ancestor directories from relativeDir toward projectDir, removing each if empty
536545
* Stops at projectDir boundary — never removes projectDir itself
537546
* @param {string} projectDir - Project root (boundary)
538547
* @param {string} relativeDir - Relative directory to start from
539548
*/
540549
async removeEmptyParents(projectDir, relativeDir) {
550+
const resolvedProject = path.resolve(projectDir);
541551
let current = relativeDir;
542552
let last = null;
543553
while (current && current !== '.' && current !== last) {
544554
last = current;
545-
const fullPath = path.join(projectDir, current);
555+
const fullPath = path.resolve(projectDir, current);
556+
// Boundary guard: never traverse outside projectDir
557+
if (!fullPath.startsWith(resolvedProject + path.sep) && fullPath !== resolvedProject) break;
546558
try {
547-
if (!(await fs.pathExists(fullPath))) break;
559+
if (!(await fs.pathExists(fullPath))) {
560+
// Dir already gone — advance current; last is reset at top of next iteration
561+
current = path.dirname(current);
562+
continue;
563+
}
548564
const remaining = await fs.readdir(fullPath);
549565
if (remaining.length > 0) break;
550566
await fs.rmdir(fullPath);
551-
} catch {
552-
break;
567+
} catch (error) {
568+
// ENOTEMPTY: TOCTOU race (file added between readdir and rmdir) — skip level, continue upward
569+
// ENOENT: dir removed by another process between pathExists and rmdir — skip level, continue upward
570+
if (error.code === 'ENOTEMPTY' || error.code === 'ENOENT') {
571+
current = path.dirname(current);
572+
continue;
573+
}
574+
break; // fatal error (e.g. EACCES) — stop upward walk
553575
}
554576
current = path.dirname(current);
555577
}

tools/cli/installers/lib/ide/platform-codes.yaml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,11 +131,14 @@ platforms:
131131
category: ide
132132
description: "OpenCode terminal coding assistant"
133133
installer:
134+
legacy_targets:
135+
- .opencode/agent
136+
- .opencode/command
134137
targets:
135-
- target_dir: .opencode/agent
138+
- target_dir: .opencode/agents
136139
template_type: opencode
137140
artifact_types: [agents]
138-
- target_dir: .opencode/command
141+
- target_dir: .opencode/commands
139142
template_type: opencode
140143
artifact_types: [workflows, tasks, tools]
141144

@@ -191,6 +194,8 @@ platforms:
191194
# template_type: string # Default template type to use
192195
# header_template: string (optional) # Override for header/frontmatter template
193196
# body_template: string (optional) # Override for body/content template
197+
# legacy_targets: array (optional) # Old target dirs to clean up on reinstall (migration)
198+
# - string # Relative path, e.g. .opencode/agent
194199
# targets: array (optional) # For multi-target installations
195200
# - target_dir: string
196201
# template_type: string

tools/cli/installers/lib/ide/templates/combined/opencode-agent.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
name: '{{name}}'
2+
mode: all
33
description: '{{description}}'
44
---
55

tools/cli/installers/lib/ide/templates/combined/opencode-task.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
---
2-
name: '{{name}}'
32
description: '{{description}}'
43
---
54

tools/cli/installers/lib/ide/templates/combined/opencode-tool.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
---
2-
name: '{{name}}'
32
description: '{{description}}'
43
---
54

tools/cli/installers/lib/ide/templates/combined/opencode-workflow-yaml.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
---
2-
name: '{{name}}'
32
description: '{{description}}'
43
---
54

tools/cli/installers/lib/ide/templates/combined/opencode-workflow.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
---
2-
name: '{{name}}'
32
description: '{{description}}'
43
---
54

0 commit comments

Comments
 (0)