diff --git a/tools/cli/installers/lib/ide/_config-driven.js b/tools/cli/installers/lib/ide/_config-driven.js index 85196cf76..d1552700f 100644 --- a/tools/cli/installers/lib/ide/_config-driven.js +++ b/tools/cli/installers/lib/ide/_config-driven.js @@ -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); } diff --git a/tools/cli/installers/lib/ide/platform-codes.yaml b/tools/cli/installers/lib/ide/platform-codes.yaml index 16723f9c5..2d9e8c129 100644 --- a/tools/cli/installers/lib/ide/platform-codes.yaml +++ b/tools/cli/installers/lib/ide/platform-codes.yaml @@ -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 diff --git a/tools/cli/installers/lib/ide/templates/combined/opencode-agent.md b/tools/cli/installers/lib/ide/templates/combined/opencode-agent.md index 65f0a771d..828d673ac 100644 --- a/tools/cli/installers/lib/ide/templates/combined/opencode-agent.md +++ b/tools/cli/installers/lib/ide/templates/combined/opencode-agent.md @@ -1,5 +1,5 @@ --- -name: '{{name}}' +mode: all description: '{{description}}' --- diff --git a/tools/cli/installers/lib/ide/templates/combined/opencode-task.md b/tools/cli/installers/lib/ide/templates/combined/opencode-task.md index 98b3a5d77..772f9c9eb 100644 --- a/tools/cli/installers/lib/ide/templates/combined/opencode-task.md +++ b/tools/cli/installers/lib/ide/templates/combined/opencode-task.md @@ -1,5 +1,4 @@ --- -name: '{{name}}' description: '{{description}}' --- diff --git a/tools/cli/installers/lib/ide/templates/combined/opencode-tool.md b/tools/cli/installers/lib/ide/templates/combined/opencode-tool.md index 1ae9c9ac8..88c317e63 100644 --- a/tools/cli/installers/lib/ide/templates/combined/opencode-tool.md +++ b/tools/cli/installers/lib/ide/templates/combined/opencode-tool.md @@ -1,5 +1,4 @@ --- -name: '{{name}}' description: '{{description}}' --- diff --git a/tools/cli/installers/lib/ide/templates/combined/opencode-workflow-yaml.md b/tools/cli/installers/lib/ide/templates/combined/opencode-workflow-yaml.md index a6f5cb96f..88838cc1c 100644 --- a/tools/cli/installers/lib/ide/templates/combined/opencode-workflow-yaml.md +++ b/tools/cli/installers/lib/ide/templates/combined/opencode-workflow-yaml.md @@ -1,5 +1,4 @@ --- -name: '{{name}}' description: '{{description}}' --- diff --git a/tools/cli/installers/lib/ide/templates/combined/opencode-workflow.md b/tools/cli/installers/lib/ide/templates/combined/opencode-workflow.md index a6f5cb96f..88838cc1c 100644 --- a/tools/cli/installers/lib/ide/templates/combined/opencode-workflow.md +++ b/tools/cli/installers/lib/ide/templates/combined/opencode-workflow.md @@ -1,5 +1,4 @@ --- -name: '{{name}}' description: '{{description}}' ---