feat: add Trae runtime install support (#1566)
* feat: add Trae runtime install support - Add Trae as a supported runtime in bin/install.js - Update README and ARCHITECTURE documentation for Trae support - Add trae-install.test.cjs test file - Update multi-runtime-select tests for Trae compatibility * feat(trae): add TRAE_CONFIG_DIR environment variable support Add support for TRAE_CONFIG_DIR environment variable as an additional way to specify the config directory for Trae runtime, following the same precedence pattern as other runtimes. * fix(trae): improve slash command conversion and subagent type mapping Update the slash command regex pattern to properly match and convert command names. Change subagent type mapping from "general-purpose" to "general_purpose_task" to match Trae's conventions. Also add comprehensive tests for Trae uninstall cleanup behavior. * docs: add Trae and Windsurf to supported runtimes in translations Update Korean, Japanese, and Portuguese README files to include Trae and Windsurf as supported runtimes in the documentation. Add installation and uninstallation instructions for Trae. * fix: update runtime selection logic and path replacements - Change 'All' shortcut from option 11 to 12 to accommodate new runtime - Update path replacement regex to handle gsd- prefix more precisely - Adjust test cases to reflect new runtime selection numbering - Add configDir to trae install options for proper path resolution * test(trae-install): add tests for getGlobalDir function Add test cases to verify behavior of getGlobalDir function with different configurations: - Default directory when no env var or explicit dir is provided - Explicit directory takes priority - Respects TRAE_CONFIG_DIR env var - Priority of explicit dir over env var - Compatibility with other runtimes
This commit is contained in:
357
bin/install.js
357
bin/install.js
@@ -69,6 +69,7 @@ const hasAntigravity = args.includes('--antigravity');
|
||||
const hasCursor = args.includes('--cursor');
|
||||
const hasWindsurf = args.includes('--windsurf');
|
||||
const hasAugment = args.includes('--augment');
|
||||
const hasTrae = args.includes('--trae');
|
||||
const hasSdk = args.includes('--sdk');
|
||||
const hasBoth = args.includes('--both'); // Legacy flag, keeps working
|
||||
const hasAll = args.includes('--all');
|
||||
@@ -77,7 +78,7 @@ const hasUninstall = args.includes('--uninstall') || args.includes('-u');
|
||||
// Runtime selection - can be set by flags or interactive prompt
|
||||
let selectedRuntimes = [];
|
||||
if (hasAll) {
|
||||
selectedRuntimes = ['claude', 'kilo', 'opencode', 'gemini', 'codex', 'copilot', 'antigravity', 'cursor', 'windsurf', 'augment'];
|
||||
selectedRuntimes = ['claude', 'kilo', 'opencode', 'gemini', 'codex', 'copilot', 'antigravity', 'cursor', 'windsurf', 'augment', 'trae'];
|
||||
} else if (hasBoth) {
|
||||
selectedRuntimes = ['claude', 'opencode'];
|
||||
} else {
|
||||
@@ -91,6 +92,7 @@ if (hasAll) {
|
||||
if (hasCursor) selectedRuntimes.push('cursor');
|
||||
if (hasWindsurf) selectedRuntimes.push('windsurf');
|
||||
if (hasAugment) selectedRuntimes.push('augment');
|
||||
if (hasTrae) selectedRuntimes.push('trae');
|
||||
}
|
||||
|
||||
// WSL + Windows Node.js detection
|
||||
@@ -138,6 +140,7 @@ function getDirName(runtime) {
|
||||
if (runtime === 'cursor') return '.cursor';
|
||||
if (runtime === 'windsurf') return '.windsurf';
|
||||
if (runtime === 'augment') return '.augment';
|
||||
if (runtime === 'trae') return '.trae';
|
||||
return '.claude';
|
||||
}
|
||||
|
||||
@@ -169,6 +172,7 @@ function getConfigDirFromHome(runtime, isGlobal) {
|
||||
if (runtime === 'cursor') return "'.cursor'";
|
||||
if (runtime === 'windsurf') return "'.windsurf'";
|
||||
if (runtime === 'augment') return "'.augment'";
|
||||
if (runtime === 'trae') return "'.trae'";
|
||||
return "'.claude'";
|
||||
}
|
||||
|
||||
@@ -182,17 +186,17 @@ function getOpencodeGlobalDir() {
|
||||
if (process.env.OPENCODE_CONFIG_DIR) {
|
||||
return expandTilde(process.env.OPENCODE_CONFIG_DIR);
|
||||
}
|
||||
|
||||
|
||||
// 2. OPENCODE_CONFIG env var (use its directory)
|
||||
if (process.env.OPENCODE_CONFIG) {
|
||||
return path.dirname(expandTilde(process.env.OPENCODE_CONFIG));
|
||||
}
|
||||
|
||||
|
||||
// 3. XDG_CONFIG_HOME/opencode
|
||||
if (process.env.XDG_CONFIG_HOME) {
|
||||
return path.join(expandTilde(process.env.XDG_CONFIG_HOME), 'opencode');
|
||||
}
|
||||
|
||||
|
||||
// 4. Default: ~/.config/opencode (XDG default)
|
||||
return path.join(os.homedir(), '.config', 'opencode');
|
||||
}
|
||||
@@ -243,7 +247,7 @@ function getGlobalDir(runtime, explicitDir = null) {
|
||||
}
|
||||
return getKiloGlobalDir();
|
||||
}
|
||||
|
||||
|
||||
if (runtime === 'gemini') {
|
||||
// Gemini: --config-dir > GEMINI_CONFIG_DIR > ~/.gemini
|
||||
if (explicitDir) {
|
||||
@@ -320,6 +324,17 @@ function getGlobalDir(runtime, explicitDir = null) {
|
||||
}
|
||||
return path.join(os.homedir(), '.augment');
|
||||
}
|
||||
if (runtime === 'trae') {
|
||||
// Trae: --config-dir > TRAE_CONFIG_DIR > ~/.trae
|
||||
if (explicitDir) {
|
||||
return expandTilde(explicitDir);
|
||||
}
|
||||
if (process.env.TRAE_CONFIG_DIR) {
|
||||
return expandTilde(process.env.TRAE_CONFIG_DIR);
|
||||
}
|
||||
return path.join(os.homedir(), '.trae');
|
||||
}
|
||||
|
||||
|
||||
// Claude Code: --config-dir > CLAUDE_CONFIG_DIR > ~/.claude
|
||||
if (explicitDir) {
|
||||
@@ -341,7 +356,7 @@ const banner = '\n' +
|
||||
'\n' +
|
||||
' Get Shit Done ' + dim + 'v' + pkg.version + reset + '\n' +
|
||||
' A meta-prompting, context engineering and spec-driven\n' +
|
||||
' development system for Claude Code, OpenCode, Gemini, Kilo, Codex, Copilot, Antigravity, Cursor, Windsurf, and Augment by TÂCHES.\n';
|
||||
' development system for Claude Code, OpenCode, Gemini, Kilo, Codex, Copilot, Antigravity, Cursor, Windsurf, Augment and Trae by TÂCHES.\n';
|
||||
|
||||
// Parse --config-dir argument
|
||||
function parseConfigDirArg() {
|
||||
@@ -379,7 +394,7 @@ if (hasUninstall) {
|
||||
|
||||
// Show help if requested
|
||||
if (hasHelp) {
|
||||
console.log(` ${yellow}Usage:${reset} npx get-shit-done-cc [options]\n\n ${yellow}Options:${reset}\n ${cyan}-g, --global${reset} Install globally (to config directory)\n ${cyan}-l, --local${reset} Install locally (to current directory)\n ${cyan}--claude${reset} Install for Claude Code only\n ${cyan}--opencode${reset} Install for OpenCode only\n ${cyan}--gemini${reset} Install for Gemini only\n ${cyan}--kilo${reset} Install for Kilo only\n ${cyan}--codex${reset} Install for Codex only\n ${cyan}--copilot${reset} Install for Copilot only\n ${cyan}--antigravity${reset} Install for Antigravity only\n ${cyan}--cursor${reset} Install for Cursor only\n ${cyan}--windsurf${reset} Install for Windsurf only\n ${cyan}--augment${reset} Install for Augment only\n ${cyan}--all${reset} Install for all runtimes\n ${cyan}--sdk${reset} Also install GSD SDK CLI (gsd-sdk)\n ${cyan}-u, --uninstall${reset} Uninstall GSD (remove all GSD files)\n ${cyan}-c, --config-dir <path>${reset} Specify custom config directory\n ${cyan}-h, --help${reset} Show this help message\n ${cyan}--force-statusline${reset} Replace existing statusline config\n\n ${yellow}Examples:${reset}\n ${dim}# Interactive install (prompts for runtime and location)${reset}\n npx get-shit-done-cc\n\n ${dim}# Install for Claude Code globally${reset}\n npx get-shit-done-cc --claude --global\n\n ${dim}# Install for Gemini globally${reset}\n npx get-shit-done-cc --gemini --global\n\n ${dim}# Install for Kilo globally${reset}\n npx get-shit-done-cc --kilo --global\n\n ${dim}# Install for Codex globally${reset}\n npx get-shit-done-cc --codex --global\n\n ${dim}# Install for Copilot globally${reset}\n npx get-shit-done-cc --copilot --global\n\n ${dim}# Install for Copilot locally${reset}\n npx get-shit-done-cc --copilot --local\n\n ${dim}# Install for Antigravity globally${reset}\n npx get-shit-done-cc --antigravity --global\n\n ${dim}# Install for Antigravity locally${reset}\n npx get-shit-done-cc --antigravity --local\n\n ${dim}# Install for Cursor globally${reset}\n npx get-shit-done-cc --cursor --global\n\n ${dim}# Install for Cursor locally${reset}\n npx get-shit-done-cc --cursor --local\n\n ${dim}# Install for Windsurf globally${reset}\n npx get-shit-done-cc --windsurf --global\n\n ${dim}# Install for Windsurf locally${reset}\n npx get-shit-done-cc --windsurf --local\n\n ${dim}# Install for Augment globally${reset}\n npx get-shit-done-cc --augment --global\n\n ${dim}# Install for Augment locally${reset}\n npx get-shit-done-cc --augment --local\n\n ${dim}# Install for all runtimes globally${reset}\n npx get-shit-done-cc --all --global\n\n ${dim}# Install to custom config directory${reset}\n npx get-shit-done-cc --kilo --global --config-dir ~/.kilo-work\n\n ${dim}# Install to current project only${reset}\n npx get-shit-done-cc --claude --local\n\n ${dim}# Uninstall GSD from Cursor globally${reset}\n npx get-shit-done-cc --cursor --global --uninstall\n\n ${yellow}Notes:${reset}\n The --config-dir option is useful when you have multiple configurations.\n It takes priority over CLAUDE_CONFIG_DIR / OPENCODE_CONFIG_DIR / GEMINI_CONFIG_DIR / KILO_CONFIG_DIR / CODEX_HOME / COPILOT_CONFIG_DIR / ANTIGRAVITY_CONFIG_DIR / CURSOR_CONFIG_DIR / WINDSURF_CONFIG_DIR / AUGMENT_CONFIG_DIR environment variables.\n`);
|
||||
console.log(` ${yellow}Usage:${reset} npx get-shit-done-cc [options]\n\n ${yellow}Options:${reset}\n ${cyan}-g, --global${reset} Install globally (to config directory)\n ${cyan}-l, --local${reset} Install locally (to current directory)\n ${cyan}--claude${reset} Install for Claude Code only\n ${cyan}--opencode${reset} Install for OpenCode only\n ${cyan}--gemini${reset} Install for Gemini only\n ${cyan}--kilo${reset} Install for Kilo only\n ${cyan}--codex${reset} Install for Codex only\n ${cyan}--copilot${reset} Install for Copilot only\n ${cyan}--antigravity${reset} Install for Antigravity only\n ${cyan}--cursor${reset} Install for Cursor only\n ${cyan}--windsurf${reset} Install for Windsurf only\n ${cyan}--augment${reset} Install for Augment only\n ${cyan}--trae${reset} Install for Trae only\n ${cyan}--all${reset} Install for all runtimes\n ${cyan}--sdk${reset} Also install GSD SDK CLI (gsd-sdk)\n ${cyan}-u, --uninstall${reset} Uninstall GSD (remove all GSD files)\n ${cyan}-c, --config-dir <path>${reset} Specify custom config directory\n ${cyan}-h, --help${reset} Show this help message\n ${cyan}--force-statusline${reset} Replace existing statusline config\n\n ${yellow}Examples:${reset}\n ${dim}# Interactive install (prompts for runtime and location)${reset}\n npx get-shit-done-cc\n\n ${dim}# Install for Claude Code globally${reset}\n npx get-shit-done-cc --claude --global\n\n ${dim}# Install for Gemini globally${reset}\n npx get-shit-done-cc --gemini --global\n\n ${dim}# Install for Kilo globally${reset}\n npx get-shit-done-cc --kilo --global\n\n ${dim}# Install for Codex globally${reset}\n npx get-shit-done-cc --codex --global\n\n ${dim}# Install for Copilot globally${reset}\n npx get-shit-done-cc --copilot --global\n\n ${dim}# Install for Copilot locally${reset}\n npx get-shit-done-cc --copilot --local\n\n ${dim}# Install for Antigravity globally${reset}\n npx get-shit-done-cc --antigravity --global\n\n ${dim}# Install for Antigravity locally${reset}\n npx get-shit-done-cc --antigravity --local\n\n ${dim}# Install for Cursor globally${reset}\n npx get-shit-done-cc --cursor --global\n\n ${dim}# Install for Cursor locally${reset}\n npx get-shit-done-cc --cursor --local\n\n ${dim}# Install for Windsurf globally${reset}\n npx get-shit-done-cc --windsurf --global\n\n ${dim}# Install for Windsurf locally${reset}\n npx get-shit-done-cc --windsurf --local\n\n ${dim}# Install for Augment globally${reset}\n npx get-shit-done-cc --augment --global\n\n ${dim}# Install for Augment locally${reset}\n npx get-shit-done-cc --augment --local\n\n ${dim}# Install for Trae globally${reset}\n npx get-shit-done-cc --trae --global\n\n ${dim}# Install for Trae locally${reset}\n npx get-shit-done-cc --trae --local\n\n ${dim}# Install for all runtimes globally${reset}\n npx get-shit-done-cc --all --global\n\n ${dim}# Install to custom config directory${reset}\n npx get-shit-done-cc --kilo --global --config-dir ~/.kilo-work\n\n ${dim}# Install to current project only${reset}\n npx get-shit-done-cc --claude --local\n\n ${dim}# Uninstall GSD from Cursor globally${reset}\n npx get-shit-done-cc --cursor --global --uninstall\n\n ${yellow}Notes:${reset}\n The --config-dir option is useful when you have multiple configurations.\n It takes priority over CLAUDE_CONFIG_DIR / OPENCODE_CONFIG_DIR / GEMINI_CONFIG_DIR / KILO_CONFIG_DIR / CODEX_HOME / COPILOT_CONFIG_DIR / ANTIGRAVITY_CONFIG_DIR / CURSOR_CONFIG_DIR / WINDSURF_CONFIG_DIR / AUGMENT_CONFIG_DIR / TRAE_CONFIG_DIR environment variables.\n`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
@@ -1407,6 +1422,61 @@ function copyCommandsAsAugmentSkills(srcDir, skillsDir, prefix, pathPrefix, runt
|
||||
recurse(srcDir, prefix);
|
||||
}
|
||||
|
||||
function convertSlashCommandsToTraeSkillMentions(content) {
|
||||
return content.replace(/\/gsd:([a-z0-9-]+)/g, (_, commandName) => {
|
||||
return `/gsd-${commandName}`;
|
||||
});
|
||||
}
|
||||
|
||||
function convertClaudeToTraeMarkdown(content) {
|
||||
let converted = convertSlashCommandsToTraeSkillMentions(content);
|
||||
converted = converted.replace(/\bBash\(/g, 'Shell(');
|
||||
converted = converted.replace(/\bEdit\(/g, 'StrReplace(');
|
||||
// Replace general-purpose subagent type with Trae's equivalent "general_purpose_task"
|
||||
converted = converted.replace(/subagent_type="general-purpose"/g, 'subagent_type="general_purpose_task"');
|
||||
converted = converted.replace(/\$ARGUMENTS\b/g, '{{GSD_ARGS}}');
|
||||
converted = converted.replace(/`\.\/CLAUDE\.md`/g, '`.trae/rules/`');
|
||||
converted = converted.replace(/\.\/CLAUDE\.md/g, '.trae/rules/');
|
||||
converted = converted.replace(/`CLAUDE\.md`/g, '`.trae/rules/`');
|
||||
converted = converted.replace(/\bCLAUDE\.md\b/g, '.trae/rules/');
|
||||
converted = converted.replace(/\.claude\/skills\//g, '.trae/skills/');
|
||||
converted = converted.replace(/\.\/\.claude\//g, './.trae/');
|
||||
converted = converted.replace(/\.claude\//g, '.trae/');
|
||||
converted = converted.replace(/\*\*Known Claude Code bug \(classifyHandoffIfNeeded\):\*\*[^\n]*\n/g, '');
|
||||
converted = converted.replace(/- \*\*classifyHandoffIfNeeded false failure:\*\*[^\n]*\n/g, '');
|
||||
converted = converted.replace(/\bClaude Code\b/g, 'Trae');
|
||||
return converted;
|
||||
}
|
||||
|
||||
function convertClaudeCommandToTraeSkill(content, skillName) {
|
||||
const converted = convertClaudeToTraeMarkdown(content);
|
||||
const { frontmatter, body } = extractFrontmatterAndBody(converted);
|
||||
let description = `Run GSD workflow ${skillName}.`;
|
||||
if (frontmatter) {
|
||||
const maybeDescription = extractFrontmatterField(frontmatter, 'description');
|
||||
if (maybeDescription) {
|
||||
description = maybeDescription;
|
||||
}
|
||||
}
|
||||
description = toSingleLine(description);
|
||||
const shortDescription = description.length > 180 ? `${description.slice(0, 177)}...` : description;
|
||||
return `---\nname: ${yamlIdentifier(skillName)}\ndescription: ${shortDescription}\n---\n${body}`;
|
||||
}
|
||||
|
||||
function convertClaudeAgentToTraeAgent(content) {
|
||||
let converted = convertClaudeToTraeMarkdown(content);
|
||||
|
||||
const { frontmatter, body } = extractFrontmatterAndBody(converted);
|
||||
if (!frontmatter) return converted;
|
||||
|
||||
const name = extractFrontmatterField(frontmatter, 'name') || 'unknown';
|
||||
const description = extractFrontmatterField(frontmatter, 'description') || '';
|
||||
|
||||
const cleanFrontmatter = `---\nname: ${yamlIdentifier(name)}\ndescription: ${yamlQuote(toSingleLine(description))}\n---`;
|
||||
|
||||
return `${cleanFrontmatter}\n${body}`;
|
||||
}
|
||||
|
||||
function convertSlashCommandsToCodexSkillMentions(content) {
|
||||
let converted = content.replace(/\/gsd:([a-z0-9-]+)/gi, (_, commandName) => {
|
||||
return `$gsd-${String(commandName).toLowerCase()}`;
|
||||
@@ -3259,7 +3329,7 @@ function convertClaudeToGeminiToml(content) {
|
||||
|
||||
const frontmatter = content.substring(3, endIndex).trim();
|
||||
const body = content.substring(endIndex + 3).trim();
|
||||
|
||||
|
||||
// Extract description from frontmatter
|
||||
let description = '';
|
||||
const lines = frontmatter.split('\n');
|
||||
@@ -3276,9 +3346,9 @@ function convertClaudeToGeminiToml(content) {
|
||||
if (description) {
|
||||
toml += `description = ${JSON.stringify(description)}\n`;
|
||||
}
|
||||
|
||||
|
||||
toml += `prompt = ${JSON.stringify(body)}\n`;
|
||||
|
||||
|
||||
return toml;
|
||||
}
|
||||
|
||||
@@ -3297,7 +3367,7 @@ function copyFlattenedCommands(srcDir, destDir, prefix, pathPrefix, runtime) {
|
||||
if (!fs.existsSync(srcDir)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Remove old gsd-*.md files before copying new ones
|
||||
if (fs.existsSync(destDir)) {
|
||||
for (const file of fs.readdirSync(destDir)) {
|
||||
@@ -3308,12 +3378,12 @@ function copyFlattenedCommands(srcDir, destDir, prefix, pathPrefix, runtime) {
|
||||
} else {
|
||||
fs.mkdirSync(destDir, { recursive: true });
|
||||
}
|
||||
|
||||
|
||||
const entries = fs.readdirSync(srcDir, { withFileTypes: true });
|
||||
|
||||
|
||||
for (const entry of entries) {
|
||||
const srcPath = path.join(srcDir, entry.name);
|
||||
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
// Recurse into subdirectories, adding to prefix
|
||||
// e.g., commands/gsd/debug/start.md -> command/gsd-debug-start.md
|
||||
@@ -3516,6 +3586,65 @@ function copyCommandsAsWindsurfSkills(srcDir, skillsDir, prefix, pathPrefix, run
|
||||
recurse(srcDir, prefix);
|
||||
}
|
||||
|
||||
function copyCommandsAsTraeSkills(srcDir, skillsDir, prefix, pathPrefix, runtime) {
|
||||
if (!fs.existsSync(srcDir)) {
|
||||
return;
|
||||
}
|
||||
|
||||
fs.mkdirSync(skillsDir, { recursive: true });
|
||||
|
||||
const existing = fs.readdirSync(skillsDir, { withFileTypes: true });
|
||||
for (const entry of existing) {
|
||||
if (entry.isDirectory() && entry.name.startsWith(`${prefix}-`)) {
|
||||
fs.rmSync(path.join(skillsDir, entry.name), { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
function recurse(currentSrcDir, currentPrefix) {
|
||||
const entries = fs.readdirSync(currentSrcDir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
const srcPath = path.join(currentSrcDir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
recurse(srcPath, `${currentPrefix}-${entry.name}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!entry.name.endsWith('.md')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const baseName = entry.name.replace('.md', '');
|
||||
const skillName = `${currentPrefix}-${baseName}`;
|
||||
const skillDir = path.join(skillsDir, skillName);
|
||||
fs.mkdirSync(skillDir, { recursive: true });
|
||||
|
||||
let content = fs.readFileSync(srcPath, 'utf8');
|
||||
const globalClaudeRegex = /~\/\.claude\//g;
|
||||
const globalClaudeHomeRegex = /\$HOME\/\.claude\//g;
|
||||
const localClaudeRegex = /\.\/\.claude\//g;
|
||||
const bareGlobalClaudeRegex = /~\/\.claude\b/g;
|
||||
const bareGlobalClaudeHomeRegex = /\$HOME\/\.claude\b/g;
|
||||
const bareLocalClaudeRegex = /\.\/\.claude\b/g;
|
||||
const traeDirRegex = /~\/\.trae\//g;
|
||||
const normalizedPathPrefix = pathPrefix.replace(/\/$/, '');
|
||||
content = content.replace(globalClaudeRegex, pathPrefix);
|
||||
content = content.replace(globalClaudeHomeRegex, pathPrefix);
|
||||
content = content.replace(localClaudeRegex, `./${getDirName(runtime)}/`);
|
||||
content = content.replace(bareGlobalClaudeRegex, normalizedPathPrefix);
|
||||
content = content.replace(bareGlobalClaudeHomeRegex, normalizedPathPrefix);
|
||||
content = content.replace(bareLocalClaudeRegex, `./${getDirName(runtime)}`);
|
||||
content = content.replace(traeDirRegex, pathPrefix);
|
||||
content = processAttribution(content, getCommitAttribution(runtime));
|
||||
content = convertClaudeCommandToTraeSkill(content, skillName);
|
||||
|
||||
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), content);
|
||||
}
|
||||
}
|
||||
|
||||
recurse(srcDir, prefix);
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy Claude commands as Copilot skills — one folder per skill with SKILL.md.
|
||||
* Applies CONV-01 (structure), CONV-02 (allowed-tools), CONV-06 (paths), CONV-07 (command names).
|
||||
@@ -3693,6 +3822,7 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime, isCommand
|
||||
const isCursor = runtime === 'cursor';
|
||||
const isWindsurf = runtime === 'windsurf';
|
||||
const isAugment = runtime === 'augment';
|
||||
const isTrae = runtime === 'trae';
|
||||
const dirName = getDirName(runtime);
|
||||
|
||||
// Clean install: remove existing destination to prevent orphaned files
|
||||
@@ -3757,6 +3887,9 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime, isCommand
|
||||
} else if (isWindsurf) {
|
||||
content = convertClaudeToWindsurfMarkdown(content);
|
||||
fs.writeFileSync(destPath, content);
|
||||
} else if (isTrae) {
|
||||
content = convertClaudeToTraeMarkdown(content);
|
||||
fs.writeFileSync(destPath, content);
|
||||
} else {
|
||||
fs.writeFileSync(destPath, content);
|
||||
}
|
||||
@@ -3786,6 +3919,15 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime, isCommand
|
||||
jsContent = jsContent.replace(/CLAUDE\.md/g, '.windsurf/rules');
|
||||
jsContent = jsContent.replace(/\bClaude Code\b/g, 'Windsurf');
|
||||
fs.writeFileSync(destPath, jsContent);
|
||||
} else if (isTrae && (entry.name.endsWith('.cjs') || entry.name.endsWith('.js'))) {
|
||||
let jsContent = fs.readFileSync(srcPath, 'utf8');
|
||||
jsContent = jsContent.replace(/\/gsd:([a-z0-9-]+)/g, (_, commandName) => {
|
||||
return `/gsd-${commandName}`;
|
||||
});
|
||||
jsContent = jsContent.replace(/\.claude\/skills\//g, '.trae/skills/');
|
||||
jsContent = jsContent.replace(/CLAUDE\.md/g, '.trae/rules/');
|
||||
jsContent = jsContent.replace(/\bClaude Code\b/g, 'Trae');
|
||||
fs.writeFileSync(destPath, jsContent);
|
||||
} else {
|
||||
fs.copyFileSync(srcPath, destPath);
|
||||
}
|
||||
@@ -3856,7 +3998,7 @@ function cleanupOrphanedHooks(settings) {
|
||||
// Only match the specific old GSD path pattern (hooks/statusline.js),
|
||||
// not third-party statusline scripts that happen to contain 'statusline.js'
|
||||
if (settings.statusLine && settings.statusLine.command &&
|
||||
/hooks[\/\\]statusline\.js/.test(settings.statusLine.command)) {
|
||||
/hooks[\/\\]statusline\.js/.test(settings.statusLine.command)) {
|
||||
settings.statusLine.command = settings.statusLine.command.replace(
|
||||
/hooks([\/\\])statusline\.js/,
|
||||
'hooks$1gsd-statusline.js'
|
||||
@@ -3962,6 +4104,7 @@ function uninstall(isGlobal, runtime = 'claude') {
|
||||
const isCursor = runtime === 'cursor';
|
||||
const isWindsurf = runtime === 'windsurf';
|
||||
const isAugment = runtime === 'augment';
|
||||
const isTrae = runtime === 'trae';
|
||||
const dirName = getDirName(runtime);
|
||||
|
||||
// Get the target directory based on runtime and install type
|
||||
@@ -3983,6 +4126,7 @@ function uninstall(isGlobal, runtime = 'claude') {
|
||||
if (runtime === 'cursor') runtimeLabel = 'Cursor';
|
||||
if (runtime === 'windsurf') runtimeLabel = 'Windsurf';
|
||||
if (runtime === 'augment') runtimeLabel = 'Augment';
|
||||
if (runtime === 'trae') runtimeLabel = 'Trae';
|
||||
|
||||
console.log(` Uninstalling GSD from ${cyan}${runtimeLabel}${reset} at ${cyan}${locationLabel}${reset}\n`);
|
||||
|
||||
@@ -4009,8 +4153,8 @@ function uninstall(isGlobal, runtime = 'claude') {
|
||||
}
|
||||
console.log(` ${green}✓${reset} Removed GSD commands from command/`);
|
||||
}
|
||||
} else if (isCodex || isCursor || isWindsurf) {
|
||||
// Codex/Cursor/Windsurf: remove skills/gsd-*/SKILL.md skill directories
|
||||
} else if (isCodex || isCursor || isWindsurf || isTrae) {
|
||||
// Codex/Cursor/Windsurf/Trae: remove skills/gsd-*/SKILL.md skill directories
|
||||
const skillsDir = path.join(targetDir, 'skills');
|
||||
if (fs.existsSync(skillsDir)) {
|
||||
let skillCount = 0;
|
||||
@@ -4029,39 +4173,39 @@ function uninstall(isGlobal, runtime = 'claude') {
|
||||
|
||||
// Codex-only: remove GSD agent .toml config files and config.toml sections
|
||||
if (isCodex) {
|
||||
const codexAgentsDir = path.join(targetDir, 'agents');
|
||||
if (fs.existsSync(codexAgentsDir)) {
|
||||
const tomlFiles = fs.readdirSync(codexAgentsDir);
|
||||
let tomlCount = 0;
|
||||
for (const file of tomlFiles) {
|
||||
if (file.startsWith('gsd-') && file.endsWith('.toml')) {
|
||||
fs.unlinkSync(path.join(codexAgentsDir, file));
|
||||
tomlCount++;
|
||||
const codexAgentsDir = path.join(targetDir, 'agents');
|
||||
if (fs.existsSync(codexAgentsDir)) {
|
||||
const tomlFiles = fs.readdirSync(codexAgentsDir);
|
||||
let tomlCount = 0;
|
||||
for (const file of tomlFiles) {
|
||||
if (file.startsWith('gsd-') && file.endsWith('.toml')) {
|
||||
fs.unlinkSync(path.join(codexAgentsDir, file));
|
||||
tomlCount++;
|
||||
}
|
||||
}
|
||||
if (tomlCount > 0) {
|
||||
removedCount++;
|
||||
console.log(` ${green}✓${reset} Removed ${tomlCount} agent .toml configs`);
|
||||
}
|
||||
}
|
||||
if (tomlCount > 0) {
|
||||
removedCount++;
|
||||
console.log(` ${green}✓${reset} Removed ${tomlCount} agent .toml configs`);
|
||||
}
|
||||
}
|
||||
|
||||
// Codex: clean GSD sections from config.toml
|
||||
const configPath = path.join(targetDir, 'config.toml');
|
||||
if (fs.existsSync(configPath)) {
|
||||
const content = fs.readFileSync(configPath, 'utf8');
|
||||
const cleaned = stripGsdFromCodexConfig(content);
|
||||
if (cleaned === null) {
|
||||
// File is empty after stripping — delete it
|
||||
fs.unlinkSync(configPath);
|
||||
removedCount++;
|
||||
console.log(` ${green}✓${reset} Removed config.toml (was GSD-only)`);
|
||||
} else if (cleaned !== content) {
|
||||
fs.writeFileSync(configPath, cleaned);
|
||||
removedCount++;
|
||||
console.log(` ${green}✓${reset} Cleaned GSD sections from config.toml`);
|
||||
// Codex: clean GSD sections from config.toml
|
||||
const configPath = path.join(targetDir, 'config.toml');
|
||||
if (fs.existsSync(configPath)) {
|
||||
const content = fs.readFileSync(configPath, 'utf8');
|
||||
const cleaned = stripGsdFromCodexConfig(content);
|
||||
if (cleaned === null) {
|
||||
// File is empty after stripping — delete it
|
||||
fs.unlinkSync(configPath);
|
||||
removedCount++;
|
||||
console.log(` ${green}✓${reset} Removed config.toml (was GSD-only)`);
|
||||
} else if (cleaned !== content) {
|
||||
fs.writeFileSync(configPath, cleaned);
|
||||
removedCount++;
|
||||
console.log(` ${green}✓${reset} Cleaned GSD sections from config.toml`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (isCopilot) {
|
||||
// Copilot: remove skills/gsd-*/ directories (same layout as Codex skills)
|
||||
const skillsDir = path.join(targetDir, 'skills');
|
||||
@@ -4298,7 +4442,7 @@ function uninstall(isGlobal, runtime = 'claude') {
|
||||
|
||||
// Remove GSD statusline if it references our hook
|
||||
if (settings.statusLine && settings.statusLine.command &&
|
||||
settings.statusLine.command.includes('gsd-statusline')) {
|
||||
settings.statusLine.command.includes('gsd-statusline')) {
|
||||
delete settings.statusLine;
|
||||
settingsModified = true;
|
||||
console.log(` ${green}✓${reset} Removed GSD statusline from settings`);
|
||||
@@ -4587,7 +4731,7 @@ function configureOpencodePermissions(isGlobal = true, configDir = null) {
|
||||
const gsdPath = opencodeConfigDir === defaultConfigDir
|
||||
? '~/.config/opencode/get-shit-done/*'
|
||||
: `${opencodeConfigDir.replace(/\\/g, '/')}/get-shit-done/*`;
|
||||
|
||||
|
||||
let modified = false;
|
||||
|
||||
// Configure read permission
|
||||
@@ -4776,6 +4920,7 @@ function writeManifest(configDir, runtime = 'claude') {
|
||||
const isAntigravity = runtime === 'antigravity';
|
||||
const isCursor = runtime === 'cursor';
|
||||
const isWindsurf = runtime === 'windsurf';
|
||||
const isTrae = runtime === 'trae';
|
||||
const gsdDir = path.join(configDir, 'get-shit-done');
|
||||
const commandsDir = path.join(configDir, 'commands', 'gsd');
|
||||
const opencodeCommandDir = path.join(configDir, 'command');
|
||||
@@ -4800,7 +4945,7 @@ function writeManifest(configDir, runtime = 'claude') {
|
||||
}
|
||||
}
|
||||
}
|
||||
if ((isCodex || isCopilot || isAntigravity || isCursor || isWindsurf || (!isOpencode && !isGemini)) && fs.existsSync(codexSkillsDir)) {
|
||||
if ((isCodex || isCopilot || isAntigravity || isCursor || isWindsurf || isTrae || (!isOpencode && !isGemini)) && fs.existsSync(codexSkillsDir)) {
|
||||
for (const skillName of listCodexSkillNames(codexSkillsDir)) {
|
||||
const skillRoot = path.join(codexSkillsDir, skillName);
|
||||
const skillHashes = generateManifest(skillRoot);
|
||||
@@ -4937,6 +5082,7 @@ function install(isGlobal, runtime = 'claude') {
|
||||
const isCursor = runtime === 'cursor';
|
||||
const isWindsurf = runtime === 'windsurf';
|
||||
const isAugment = runtime === 'augment';
|
||||
const isTrae = runtime === 'trae';
|
||||
const dirName = getDirName(runtime);
|
||||
const src = path.join(__dirname, '..');
|
||||
|
||||
@@ -4970,6 +5116,7 @@ function install(isGlobal, runtime = 'claude') {
|
||||
if (isCursor) runtimeLabel = 'Cursor';
|
||||
if (isWindsurf) runtimeLabel = 'Windsurf';
|
||||
if (isAugment) runtimeLabel = 'Augment';
|
||||
if (isTrae) runtimeLabel = 'Trae';
|
||||
|
||||
console.log(` Installing for ${cyan}${runtimeLabel}${reset} to ${cyan}${locationLabel}${reset}\n`);
|
||||
|
||||
@@ -4987,7 +5134,7 @@ function install(isGlobal, runtime = 'claude') {
|
||||
// OpenCode/Kilo: flat structure in command/ directory
|
||||
const commandDir = path.join(targetDir, 'command');
|
||||
fs.mkdirSync(commandDir, { recursive: true });
|
||||
|
||||
|
||||
// Copy commands/gsd/*.md as command/gsd-*.md (flatten structure)
|
||||
const gsdSrc = path.join(src, 'commands', 'gsd');
|
||||
copyFlattenedCommands(gsdSrc, commandDir, 'gsd', pathPrefix, runtime);
|
||||
@@ -5067,6 +5214,16 @@ function install(isGlobal, runtime = 'claude') {
|
||||
} else {
|
||||
failures.push('skills/gsd-*');
|
||||
}
|
||||
} else if (isTrae) {
|
||||
const skillsDir = path.join(targetDir, 'skills');
|
||||
const gsdSrc = path.join(src, 'commands', 'gsd');
|
||||
copyCommandsAsTraeSkills(gsdSrc, skillsDir, 'gsd', pathPrefix, runtime);
|
||||
const installedSkillNames = listCodexSkillNames(skillsDir);
|
||||
if (installedSkillNames.length > 0) {
|
||||
console.log(` ${green}✓${reset} Installed ${installedSkillNames.length} skills to skills/`);
|
||||
} else {
|
||||
failures.push('skills/gsd-*');
|
||||
}
|
||||
} else if (isGemini) {
|
||||
const commandsDir = path.join(targetDir, 'commands');
|
||||
fs.mkdirSync(commandsDir, { recursive: true });
|
||||
@@ -5136,9 +5293,14 @@ function install(isGlobal, runtime = 'claude') {
|
||||
// Replace ~/.claude/ and $HOME/.claude/ as they are the source of truth in the repo
|
||||
const dirRegex = /~\/\.claude\//g;
|
||||
const homeDirRegex = /\$HOME\/\.claude\//g;
|
||||
const bareDirRegex = /~\/\.claude\b/g;
|
||||
const bareHomeDirRegex = /\$HOME\/\.claude\b/g;
|
||||
const normalizedPathPrefix = pathPrefix.replace(/\/$/, '');
|
||||
if (!isCopilot && !isAntigravity) {
|
||||
content = content.replace(dirRegex, pathPrefix);
|
||||
content = content.replace(homeDirRegex, pathPrefix);
|
||||
content = content.replace(bareDirRegex, normalizedPathPrefix);
|
||||
content = content.replace(bareHomeDirRegex, normalizedPathPrefix);
|
||||
}
|
||||
content = processAttribution(content, getCommitAttribution(runtime));
|
||||
// Convert frontmatter for runtime compatibility (agents need different handling)
|
||||
@@ -5160,6 +5322,8 @@ function install(isGlobal, runtime = 'claude') {
|
||||
content = convertClaudeAgentToWindsurfAgent(content);
|
||||
} else if (isAugment) {
|
||||
content = convertClaudeAgentToAugmentAgent(content);
|
||||
} else if (isTrae) {
|
||||
content = convertClaudeAgentToTraeAgent(content);
|
||||
}
|
||||
const destName = isCopilot ? entry.name.replace('.md', '.agent.md') : entry.name;
|
||||
fs.writeFileSync(path.join(agentsDest, destName), content);
|
||||
@@ -5193,7 +5357,7 @@ function install(isGlobal, runtime = 'claude') {
|
||||
failures.push('VERSION');
|
||||
}
|
||||
|
||||
if (!isCodex && !isCopilot && !isCursor && !isWindsurf) {
|
||||
if (!isCodex && !isCopilot && !isCursor && !isWindsurf && !isTrae) {
|
||||
// Write package.json to force CommonJS mode for GSD scripts
|
||||
// Prevents "require is not defined" errors when project has "type": "module"
|
||||
// Node.js walks up looking for package.json - this stops inheritance from project
|
||||
@@ -5359,6 +5523,11 @@ function install(isGlobal, runtime = 'claude') {
|
||||
return { settingsPath: null, settings: null, statuslineCommand: null, runtime, configDir: targetDir };
|
||||
}
|
||||
|
||||
if (isTrae) {
|
||||
// Trae uses skills — no settings.json hooks needed
|
||||
return { settingsPath: null, settings: null, statuslineCommand: null, runtime, configDir: targetDir };
|
||||
}
|
||||
|
||||
// Configure statusline and hooks in settings.json
|
||||
// Gemini and Antigravity use AfterTool instead of PostToolUse for post-tool hooks
|
||||
const postToolEvent = (runtime === 'gemini' || runtime === 'antigravity') ? 'AfterTool' : 'PostToolUse';
|
||||
@@ -5593,8 +5762,9 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
|
||||
const isCopilot = runtime === 'copilot';
|
||||
const isCursor = runtime === 'cursor';
|
||||
const isWindsurf = runtime === 'windsurf';
|
||||
const isTrae = runtime === 'trae';
|
||||
|
||||
if (shouldInstallStatusline && !isOpencode && !isKilo && !isCodex && !isCopilot && !isCursor && !isWindsurf) {
|
||||
if (shouldInstallStatusline && !isOpencode && !isKilo && !isCodex && !isCopilot && !isCursor && !isWindsurf && !isTrae) {
|
||||
settings.statusLine = {
|
||||
type: 'command',
|
||||
command: statuslineCommand
|
||||
@@ -5603,7 +5773,7 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
|
||||
}
|
||||
|
||||
// Write settings when runtime supports settings.json
|
||||
if (!isCodex && !isCopilot && !isKilo && !isCursor && !isWindsurf) {
|
||||
if (!isCodex && !isCopilot && !isKilo && !isCursor && !isWindsurf && !isTrae) {
|
||||
writeSettings(settingsPath, settings);
|
||||
}
|
||||
|
||||
@@ -5648,6 +5818,7 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
|
||||
if (runtime === 'cursor') program = 'Cursor';
|
||||
if (runtime === 'windsurf') program = 'Windsurf';
|
||||
if (runtime === 'augment') program = 'Augment';
|
||||
if (runtime === 'trae') program = 'Trae';
|
||||
|
||||
let command = '/gsd:new-project';
|
||||
if (runtime === 'opencode') command = '/gsd-new-project';
|
||||
@@ -5658,6 +5829,7 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
|
||||
if (runtime === 'cursor') command = 'gsd-new-project (mention the skill name)';
|
||||
if (runtime === 'windsurf') command = '/gsd-new-project';
|
||||
if (runtime === 'augment') command = '/gsd-new-project';
|
||||
if (runtime === 'trae') command = '/gsd-new-project';
|
||||
console.log(`
|
||||
${green}Done!${reset} Open a blank directory in ${program} and run ${cyan}${command}${reset}.
|
||||
|
||||
@@ -5810,9 +5982,10 @@ function promptRuntime(callback) {
|
||||
'7': 'antigravity',
|
||||
'8': 'cursor',
|
||||
'9': 'windsurf',
|
||||
'10': 'augment'
|
||||
'10': 'augment',
|
||||
'11': 'trae'
|
||||
};
|
||||
const allRuntimes = ['claude', 'kilo', 'opencode', 'gemini', 'codex', 'copilot', 'antigravity', 'cursor', 'windsurf', 'augment'];
|
||||
const allRuntimes = ['claude', 'kilo', 'opencode', 'gemini', 'codex', 'copilot', 'antigravity', 'cursor', 'windsurf', 'augment', 'trae'];
|
||||
|
||||
console.log(` ${yellow}Which runtime(s) would you like to install for?${reset}\n\n ${cyan}1${reset}) Claude Code ${dim}(~/.claude)${reset}
|
||||
${cyan}2${reset}) Kilo ${dim}(~/.config/kilo)${reset} - open source, the #1 AI coding platform on OpenRouter
|
||||
@@ -5824,7 +5997,8 @@ function promptRuntime(callback) {
|
||||
${cyan}8${reset}) Cursor ${dim}(~/.cursor)${reset}
|
||||
${cyan}9${reset}) Windsurf ${dim}(~/.windsurf)${reset}
|
||||
${cyan}10${reset}) Augment ${dim}(~/.augment)${reset}
|
||||
${cyan}11${reset}) All
|
||||
${cyan}11${reset}) Trae ${dim}(~/.trae)${reset}
|
||||
${cyan}12${reset}) All
|
||||
|
||||
${dim}Select multiple: 1,2,6 or 1 2 6${reset}
|
||||
`);
|
||||
@@ -5835,7 +6009,7 @@ function promptRuntime(callback) {
|
||||
const input = answer.trim() || '1';
|
||||
|
||||
// "All" shortcut
|
||||
if (input === '11') {
|
||||
if (input === '12') {
|
||||
callback(allRuntimes);
|
||||
return;
|
||||
}
|
||||
@@ -5966,6 +6140,7 @@ if (process.env.GSD_TEST_MODE) {
|
||||
mergeCodexConfig,
|
||||
installCodexConfig,
|
||||
install,
|
||||
uninstall,
|
||||
convertClaudeCommandToCodexSkill,
|
||||
convertClaudeToOpencodeFrontmatter,
|
||||
convertClaudeToKiloFrontmatter,
|
||||
@@ -6002,6 +6177,10 @@ if (process.env.GSD_TEST_MODE) {
|
||||
convertClaudeCommandToAugmentSkill,
|
||||
convertClaudeAgentToAugmentAgent,
|
||||
copyCommandsAsAugmentSkills,
|
||||
convertClaudeToTraeMarkdown,
|
||||
convertClaudeCommandToTraeSkill,
|
||||
convertClaudeAgentToTraeAgent,
|
||||
copyCommandsAsTraeSkills,
|
||||
writeManifest,
|
||||
reportLocalPatches,
|
||||
validateHookFields,
|
||||
@@ -6010,41 +6189,41 @@ if (process.env.GSD_TEST_MODE) {
|
||||
};
|
||||
} else {
|
||||
|
||||
// Main logic
|
||||
if (hasGlobal && hasLocal) {
|
||||
console.error(` ${yellow}Cannot specify both --global and --local${reset}`);
|
||||
process.exit(1);
|
||||
} else if (explicitConfigDir && hasLocal) {
|
||||
console.error(` ${yellow}Cannot use --config-dir with --local${reset}`);
|
||||
process.exit(1);
|
||||
} else if (hasUninstall) {
|
||||
if (!hasGlobal && !hasLocal) {
|
||||
console.error(` ${yellow}--uninstall requires --global or --local${reset}`);
|
||||
// Main logic
|
||||
if (hasGlobal && hasLocal) {
|
||||
console.error(` ${yellow}Cannot specify both --global and --local${reset}`);
|
||||
process.exit(1);
|
||||
}
|
||||
const runtimes = selectedRuntimes.length > 0 ? selectedRuntimes : ['claude'];
|
||||
for (const runtime of runtimes) {
|
||||
uninstall(hasGlobal, runtime);
|
||||
}
|
||||
} else if (selectedRuntimes.length > 0) {
|
||||
if (!hasGlobal && !hasLocal) {
|
||||
promptLocation(selectedRuntimes);
|
||||
} else if (explicitConfigDir && hasLocal) {
|
||||
console.error(` ${yellow}Cannot use --config-dir with --local${reset}`);
|
||||
process.exit(1);
|
||||
} else if (hasUninstall) {
|
||||
if (!hasGlobal && !hasLocal) {
|
||||
console.error(` ${yellow}--uninstall requires --global or --local${reset}`);
|
||||
process.exit(1);
|
||||
}
|
||||
const runtimes = selectedRuntimes.length > 0 ? selectedRuntimes : ['claude'];
|
||||
for (const runtime of runtimes) {
|
||||
uninstall(hasGlobal, runtime);
|
||||
}
|
||||
} else if (selectedRuntimes.length > 0) {
|
||||
if (!hasGlobal && !hasLocal) {
|
||||
promptLocation(selectedRuntimes);
|
||||
} else {
|
||||
installAllRuntimes(selectedRuntimes, hasGlobal, false);
|
||||
}
|
||||
} else if (hasGlobal || hasLocal) {
|
||||
// Default to Claude if no runtime specified but location is
|
||||
installAllRuntimes(['claude'], hasGlobal, false);
|
||||
} else {
|
||||
installAllRuntimes(selectedRuntimes, hasGlobal, false);
|
||||
// Interactive
|
||||
if (!process.stdin.isTTY) {
|
||||
console.log(` ${yellow}Non-interactive terminal detected, defaulting to Claude Code global install${reset}\n`);
|
||||
installAllRuntimes(['claude'], true, false);
|
||||
} else {
|
||||
promptRuntime((runtimes) => {
|
||||
promptLocation(runtimes);
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (hasGlobal || hasLocal) {
|
||||
// Default to Claude if no runtime specified but location is
|
||||
installAllRuntimes(['claude'], hasGlobal, false);
|
||||
} else {
|
||||
// Interactive
|
||||
if (!process.stdin.isTTY) {
|
||||
console.log(` ${yellow}Non-interactive terminal detected, defaulting to Claude Code global install${reset}\n`);
|
||||
installAllRuntimes(['claude'], true, false);
|
||||
} else {
|
||||
promptRuntime((runtimes) => {
|
||||
promptLocation(runtimes);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
} // end of else block for GSD_TEST_MODE
|
||||
|
||||
Reference in New Issue
Block a user