feat: add Kilo CLI runtime support
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
|
||||
[English](README.md) · [Português](README.pt-BR.md) · [简体中文](README.zh-CN.md) · **日本語**
|
||||
|
||||
**Claude Code、OpenCode、Gemini CLI、Codex、Copilot、Antigravity向けの軽量かつ強力なメタプロンプティング、コンテキストエンジニアリング、仕様駆動開発システム。**
|
||||
**Claude Code、OpenCode、Gemini CLI、Kilo、Codex、Copilot、Antigravity向けの軽量かつ強力なメタプロンプティング、コンテキストエンジニアリング、仕様駆動開発システム。**
|
||||
|
||||
**コンテキストロット(Claudeがコンテキストウィンドウを消費するにつれ品質が劣化する現象)を解決します。**
|
||||
|
||||
@@ -84,12 +84,13 @@ npx get-shit-done-cc@latest
|
||||
```
|
||||
|
||||
インストーラーが以下の選択を求めます:
|
||||
1. **ランタイム** — Claude Code、OpenCode、Gemini、Codex、Copilot、Cursor、Antigravity、またはすべて(インタラクティブ複数選択 — 1回のインストールセッションで複数のランタイムを選択可能)
|
||||
1. **ランタイム** — Claude Code、OpenCode、Gemini、Kilo、Codex、Copilot、Cursor、Antigravity、またはすべて(インタラクティブ複数選択 — 1回のインストールセッションで複数のランタイムを選択可能)
|
||||
2. **インストール先** — グローバル(全プロジェクト)またはローカル(現在のプロジェクトのみ)
|
||||
|
||||
確認方法:
|
||||
- Claude Code / Gemini: `/gsd:help`
|
||||
- OpenCode: `/gsd-help`
|
||||
- Kilo: `/gsd-help`
|
||||
- Codex: `$gsd-help`
|
||||
- Copilot: `/gsd:help`
|
||||
- Antigravity: `/gsd:help`
|
||||
@@ -119,6 +120,10 @@ npx get-shit-done-cc --opencode --global # ~/.config/opencode/ にインスト
|
||||
# Gemini CLI
|
||||
npx get-shit-done-cc --gemini --global # ~/.gemini/ にインストール
|
||||
|
||||
# Kilo(OpenCodeフォーク)
|
||||
npx get-shit-done-cc --kilo --global # ~/.config/kilo/ にインストール
|
||||
npx get-shit-done-cc --kilo --local # ./.kilo/ にインストール
|
||||
|
||||
# Codex(スキルファースト)
|
||||
npx get-shit-done-cc --codex --global # ~/.codex/ にインストール
|
||||
npx get-shit-done-cc --codex --local # ./.codex/ にインストール
|
||||
@@ -140,7 +145,7 @@ npx get-shit-done-cc --all --global # すべてのディレクトリにイ
|
||||
```
|
||||
|
||||
`--global`(`-g`)または `--local`(`-l`)でインストール先の質問をスキップできます。
|
||||
`--claude`、`--opencode`、`--gemini`、`--codex`、`--copilot`、`--cursor`、`--antigravity`、または `--all` でランタイムの質問をスキップできます。
|
||||
`--claude`、`--opencode`、`--gemini`、`--kilo`、`--codex`、`--copilot`、`--cursor`、`--antigravity`、または `--all` でランタイムの質問をスキップできます。
|
||||
|
||||
</details>
|
||||
|
||||
@@ -777,6 +782,7 @@ GSDを完全に削除するには:
|
||||
npx get-shit-done-cc --claude --global --uninstall
|
||||
npx get-shit-done-cc --opencode --global --uninstall
|
||||
npx get-shit-done-cc --gemini --global --uninstall
|
||||
npx get-shit-done-cc --kilo --global --uninstall
|
||||
npx get-shit-done-cc --codex --global --uninstall
|
||||
npx get-shit-done-cc --copilot --global --uninstall
|
||||
npx get-shit-done-cc --cursor --global --uninstall
|
||||
@@ -786,6 +792,7 @@ npx get-shit-done-cc --antigravity --global --uninstall
|
||||
npx get-shit-done-cc --claude --local --uninstall
|
||||
npx get-shit-done-cc --opencode --local --uninstall
|
||||
npx get-shit-done-cc --gemini --local --uninstall
|
||||
npx get-shit-done-cc --kilo --local --uninstall
|
||||
npx get-shit-done-cc --codex --local --uninstall
|
||||
npx get-shit-done-cc --copilot --local --uninstall
|
||||
npx get-shit-done-cc --cursor --local --uninstall
|
||||
@@ -798,7 +805,7 @@ npx get-shit-done-cc --antigravity --local --uninstall
|
||||
|
||||
## コミュニティポート
|
||||
|
||||
OpenCode、Gemini CLI、Codexは `npx get-shit-done-cc` でネイティブサポートされています。
|
||||
OpenCode、Gemini CLI、Kilo、Codexは `npx get-shit-done-cc` でネイティブサポートされています。
|
||||
|
||||
以下のコミュニティポートがマルチランタイムサポートの先駆けとなりました:
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
[English](README.md) · [Português](README.pt-BR.md) · [简体中文](README.zh-CN.md) · [日本語](README.ja-JP.md) · **한국어**
|
||||
|
||||
**Claude Code, OpenCode, Gemini CLI, Codex, Copilot, Cursor, Antigravity를 위한 가볍고 강력한 메타 프롬프팅, 컨텍스트 엔지니어링, 스펙 기반 개발 시스템.**
|
||||
**Claude Code, OpenCode, Gemini CLI, Kilo, Codex, Copilot, Cursor, Antigravity를 위한 가볍고 강력한 메타 프롬프팅, 컨텍스트 엔지니어링, 스펙 기반 개발 시스템.**
|
||||
|
||||
**컨텍스트 rot를 해결합니다 — Claude의 컨텍스트 창이 채워질수록 품질이 저하되는 문제.**
|
||||
|
||||
@@ -82,12 +82,13 @@ npx get-shit-done-cc@latest
|
||||
```
|
||||
|
||||
설치 중에 다음을 선택합니다:
|
||||
1. **런타임** — Claude Code, OpenCode, Gemini, Codex, Copilot, Cursor, Antigravity, 또는 전체 (대화형 다중 선택 — 한 번에 여러 런타임 선택 가능)
|
||||
1. **런타임** — Claude Code, OpenCode, Gemini, Kilo, Codex, Copilot, Cursor, Antigravity, 또는 전체 (대화형 다중 선택 — 한 번에 여러 런타임 선택 가능)
|
||||
2. **위치** — 전역 (모든 프로젝트) 또는 로컬 (현재 프로젝트만)
|
||||
|
||||
설치가 됐는지 확인하려면:
|
||||
- Claude Code / Gemini: `/gsd:help`
|
||||
- OpenCode: `/gsd-help`
|
||||
- Kilo: `/gsd-help`
|
||||
- Codex: `$gsd-help`
|
||||
- Copilot: `/gsd:help`
|
||||
- Antigravity: `/gsd:help`
|
||||
@@ -117,6 +118,10 @@ npx get-shit-done-cc --opencode --global # ~/.config/opencode/에 설치
|
||||
# Gemini CLI
|
||||
npx get-shit-done-cc --gemini --global # ~/.gemini/에 설치
|
||||
|
||||
# Kilo (OpenCode 포크)
|
||||
npx get-shit-done-cc --kilo --global # ~/.config/kilo/에 설치
|
||||
npx get-shit-done-cc --kilo --local # ./.kilo/에 설치
|
||||
|
||||
# Codex (스킬 우선)
|
||||
npx get-shit-done-cc --codex --global # ~/.codex/에 설치
|
||||
npx get-shit-done-cc --codex --local # ./.codex/에 설치
|
||||
@@ -138,7 +143,7 @@ npx get-shit-done-cc --all --global # 모든 디렉터리에 설치
|
||||
```
|
||||
|
||||
위치 프롬프트 건너뛰기: `--global` (`-g`) 또는 `--local` (`-l`).
|
||||
런타임 프롬프트 건너뛰기: `--claude`, `--opencode`, `--gemini`, `--codex`, `--copilot`, `--cursor`, `--antigravity`, 또는 `--all`.
|
||||
런타임 프롬프트 건너뛰기: `--claude`, `--opencode`, `--gemini`, `--kilo`, `--codex`, `--copilot`, `--cursor`, `--antigravity`, 또는 `--all`.
|
||||
|
||||
</details>
|
||||
|
||||
@@ -766,6 +771,7 @@ GSD를 완전히 제거하려면:
|
||||
npx get-shit-done-cc --claude --global --uninstall
|
||||
npx get-shit-done-cc --opencode --global --uninstall
|
||||
npx get-shit-done-cc --gemini --global --uninstall
|
||||
npx get-shit-done-cc --kilo --global --uninstall
|
||||
npx get-shit-done-cc --codex --global --uninstall
|
||||
npx get-shit-done-cc --copilot --global --uninstall
|
||||
npx get-shit-done-cc --cursor --global --uninstall
|
||||
@@ -775,6 +781,7 @@ npx get-shit-done-cc --antigravity --global --uninstall
|
||||
npx get-shit-done-cc --claude --local --uninstall
|
||||
npx get-shit-done-cc --opencode --local --uninstall
|
||||
npx get-shit-done-cc --gemini --local --uninstall
|
||||
npx get-shit-done-cc --kilo --local --uninstall
|
||||
npx get-shit-done-cc --codex --local --uninstall
|
||||
npx get-shit-done-cc --copilot --local --uninstall
|
||||
npx get-shit-done-cc --cursor --local --uninstall
|
||||
@@ -787,7 +794,7 @@ npx get-shit-done-cc --antigravity --local --uninstall
|
||||
|
||||
## 커뮤니티 포트
|
||||
|
||||
OpenCode, Gemini CLI, Codex는 이제 `npx get-shit-done-cc`를 통해 기본 지원됩니다.
|
||||
OpenCode, Gemini CLI, Kilo, Codex는 이제 `npx get-shit-done-cc`를 통해 기본 지원됩니다.
|
||||
|
||||
이 커뮤니티 포트들이 멀티 런타임 지원의 선구자였습니다:
|
||||
|
||||
|
||||
15
README.md
15
README.md
@@ -4,7 +4,7 @@
|
||||
|
||||
**English** · [Português](README.pt-BR.md) · [简体中文](README.zh-CN.md) · [日本語](README.ja-JP.md) · [한국어](README.ko-KR.md)
|
||||
|
||||
**A light-weight and powerful meta-prompting, context engineering and spec-driven development system for Claude Code, OpenCode, Gemini CLI, Codex, Copilot, Cursor, Windsurf, and Antigravity.**
|
||||
**A light-weight and powerful meta-prompting, context engineering and spec-driven development system for Claude Code, OpenCode, Gemini CLI, Kilo, Codex, Copilot, Cursor, Windsurf, and Antigravity.**
|
||||
|
||||
**Solves context rot — the quality degradation that happens as Claude fills its context window.**
|
||||
|
||||
@@ -82,12 +82,13 @@ npx get-shit-done-cc@latest
|
||||
```
|
||||
|
||||
The installer prompts you to choose:
|
||||
1. **Runtime** — Claude Code, OpenCode, Gemini, Codex, Copilot, Cursor, Windsurf, Antigravity, or all (interactive multi-select — pick multiple runtimes in a single install session)
|
||||
1. **Runtime** — Claude Code, OpenCode, Gemini, Kilo, Codex, Copilot, Cursor, Windsurf, Antigravity, or all (interactive multi-select — pick multiple runtimes in a single install session)
|
||||
2. **Location** — Global (all projects) or local (current project only)
|
||||
|
||||
Verify with:
|
||||
- Claude Code / Gemini: `/gsd:help`
|
||||
- OpenCode: `/gsd-help`
|
||||
- Kilo: `/gsd-help`
|
||||
- Codex: `$gsd-help`
|
||||
- Copilot: `/gsd:help`
|
||||
- Antigravity: `/gsd:help`
|
||||
@@ -117,6 +118,10 @@ npx get-shit-done-cc --opencode --global # Install to ~/.config/opencode/
|
||||
# Gemini CLI
|
||||
npx get-shit-done-cc --gemini --global # Install to ~/.gemini/
|
||||
|
||||
# Kilo (OpenCode fork)
|
||||
npx get-shit-done-cc --kilo --global # Install to ~/.config/kilo/
|
||||
npx get-shit-done-cc --kilo --local # Install to ./.kilo/
|
||||
|
||||
# Codex (skills-first)
|
||||
npx get-shit-done-cc --codex --global # Install to ~/.codex/
|
||||
npx get-shit-done-cc --codex --local # Install to ./.codex/
|
||||
@@ -142,7 +147,7 @@ npx get-shit-done-cc --all --global # Install to all directories
|
||||
```
|
||||
|
||||
Use `--global` (`-g`) or `--local` (`-l`) to skip the location prompt.
|
||||
Use `--claude`, `--opencode`, `--gemini`, `--codex`, `--copilot`, `--cursor`, `--windsurf`, `--antigravity`, or `--all` to skip the runtime prompt.
|
||||
Use `--claude`, `--opencode`, `--gemini`, `--kilo`, `--codex`, `--copilot`, `--cursor`, `--windsurf`, `--antigravity`, or `--all` to skip the runtime prompt.
|
||||
Use `--sdk` to also install the GSD SDK CLI (`gsd-sdk`) for headless autonomous execution.
|
||||
|
||||
</details>
|
||||
@@ -787,6 +792,7 @@ To remove GSD completely:
|
||||
npx get-shit-done-cc --claude --global --uninstall
|
||||
npx get-shit-done-cc --opencode --global --uninstall
|
||||
npx get-shit-done-cc --gemini --global --uninstall
|
||||
npx get-shit-done-cc --kilo --global --uninstall
|
||||
npx get-shit-done-cc --codex --global --uninstall
|
||||
npx get-shit-done-cc --copilot --global --uninstall
|
||||
npx get-shit-done-cc --cursor --global --uninstall
|
||||
@@ -797,6 +803,7 @@ npx get-shit-done-cc --antigravity --global --uninstall
|
||||
npx get-shit-done-cc --claude --local --uninstall
|
||||
npx get-shit-done-cc --opencode --local --uninstall
|
||||
npx get-shit-done-cc --gemini --local --uninstall
|
||||
npx get-shit-done-cc --kilo --local --uninstall
|
||||
npx get-shit-done-cc --codex --local --uninstall
|
||||
npx get-shit-done-cc --copilot --local --uninstall
|
||||
npx get-shit-done-cc --cursor --local --uninstall
|
||||
@@ -810,7 +817,7 @@ This removes all GSD commands, agents, hooks, and settings while preserving your
|
||||
|
||||
## Community Ports
|
||||
|
||||
OpenCode, Gemini CLI, and Codex are now natively supported via `npx get-shit-done-cc`.
|
||||
OpenCode, Gemini CLI, Kilo, and Codex are now natively supported via `npx get-shit-done-cc`.
|
||||
|
||||
These community ports pioneered multi-runtime support:
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
[English](README.md) · **Português** · [简体中文](README.zh-CN.md) · [日本語](README.ja-JP.md)
|
||||
|
||||
**Um sistema leve e poderoso de meta-prompting, engenharia de contexto e desenvolvimento orientado a especificação para Claude Code, OpenCode, Gemini CLI, Codex, Copilot, Cursor e Antigravity.**
|
||||
**Um sistema leve e poderoso de meta-prompting, engenharia de contexto e desenvolvimento orientado a especificação para Claude Code, OpenCode, Gemini CLI, Kilo, Codex, Copilot, Cursor e Antigravity.**
|
||||
|
||||
**Resolve context rot — a degradação de qualidade que acontece conforme o Claude enche a janela de contexto.**
|
||||
|
||||
@@ -80,12 +80,13 @@ npx get-shit-done-cc@latest
|
||||
```
|
||||
|
||||
O instalador pede:
|
||||
1. **Runtime** — Claude Code, OpenCode, Gemini, Codex, Copilot, Cursor, Antigravity, ou todos
|
||||
1. **Runtime** — Claude Code, OpenCode, Gemini, Kilo, Codex, Copilot, Cursor, Antigravity, ou todos
|
||||
2. **Local** — Global (todos os projetos) ou local (apenas projeto atual)
|
||||
|
||||
Verifique com:
|
||||
- Claude Code / Gemini: `/gsd:help`
|
||||
- OpenCode: `/gsd-help`
|
||||
- Kilo: `/gsd-help`
|
||||
- Codex: `$gsd-help`
|
||||
- Copilot: `/gsd:help`
|
||||
- Antigravity: `/gsd:help`
|
||||
@@ -113,6 +114,10 @@ npx get-shit-done-cc --opencode --global
|
||||
# Gemini CLI
|
||||
npx get-shit-done-cc --gemini --global
|
||||
|
||||
# Kilo
|
||||
npx get-shit-done-cc --kilo --global
|
||||
npx get-shit-done-cc --kilo --local
|
||||
|
||||
# Codex
|
||||
npx get-shit-done-cc --codex --global
|
||||
npx get-shit-done-cc --codex --local
|
||||
@@ -134,7 +139,7 @@ npx get-shit-done-cc --all --global
|
||||
```
|
||||
|
||||
Use `--global` (`-g`) ou `--local` (`-l`) para pular a pergunta de local.
|
||||
Use `--claude`, `--opencode`, `--gemini`, `--codex`, `--copilot`, `--cursor`, `--antigravity` ou `--all` para pular a pergunta de runtime.
|
||||
Use `--claude`, `--opencode`, `--gemini`, `--kilo`, `--codex`, `--copilot`, `--cursor`, `--antigravity` ou `--all` para pular a pergunta de runtime.
|
||||
|
||||
</details>
|
||||
|
||||
@@ -399,6 +404,7 @@ CLAUDE_CONFIG_DIR=/home/youruser/.claude npx get-shit-done-cc --global
|
||||
npx get-shit-done-cc --claude --global --uninstall
|
||||
npx get-shit-done-cc --opencode --global --uninstall
|
||||
npx get-shit-done-cc --gemini --global --uninstall
|
||||
npx get-shit-done-cc --kilo --global --uninstall
|
||||
npx get-shit-done-cc --codex --global --uninstall
|
||||
npx get-shit-done-cc --copilot --global --uninstall
|
||||
npx get-shit-done-cc --cursor --global --uninstall
|
||||
@@ -408,6 +414,7 @@ npx get-shit-done-cc --antigravity --global --uninstall
|
||||
npx get-shit-done-cc --claude --local --uninstall
|
||||
npx get-shit-done-cc --opencode --local --uninstall
|
||||
npx get-shit-done-cc --gemini --local --uninstall
|
||||
npx get-shit-done-cc --kilo --local --uninstall
|
||||
npx get-shit-done-cc --codex --local --uninstall
|
||||
npx get-shit-done-cc --copilot --local --uninstall
|
||||
npx get-shit-done-cc --cursor --local --uninstall
|
||||
@@ -418,7 +425,7 @@ npx get-shit-done-cc --antigravity --local --uninstall
|
||||
|
||||
## Community Ports
|
||||
|
||||
OpenCode, Gemini CLI e Codex agora são suportados nativamente via `npx get-shit-done-cc`.
|
||||
OpenCode, Gemini CLI, Kilo e Codex agora são suportados nativamente via `npx get-shit-done-cc`.
|
||||
|
||||
| Projeto | Plataforma | Descrição |
|
||||
|---------|------------|-----------|
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
[English](README.md) · [Português](README.pt-BR.md) · **简体中文** · [日本語](README.ja-JP.md)
|
||||
|
||||
**一个轻量但强大的元提示、上下文工程与规格驱动开发系统,适用于 Claude Code、OpenCode、Gemini CLI、Codex、Copilot、Cursor 和 Antigravity。**
|
||||
**一个轻量但强大的元提示、上下文工程与规格驱动开发系统,适用于 Claude Code、OpenCode、Gemini CLI、Kilo、Codex、Copilot、Cursor 和 Antigravity。**
|
||||
|
||||
**它解决的是 context rot:随着 Claude 的上下文窗口被填满,输出质量逐步劣化的问题。**
|
||||
|
||||
@@ -82,12 +82,13 @@ npx get-shit-done-cc@latest
|
||||
```
|
||||
|
||||
安装器会提示你选择:
|
||||
1. **运行时**:Claude Code、OpenCode、Gemini、Codex、Copilot、Cursor、Antigravity,或全部
|
||||
1. **运行时**:Claude Code、OpenCode、Gemini、Kilo、Codex、Copilot、Cursor、Antigravity,或全部
|
||||
2. **安装位置**:全局(所有项目)或本地(仅当前项目)
|
||||
|
||||
安装后可这样验证:
|
||||
- Claude Code / Gemini:`/gsd:help`
|
||||
- OpenCode:`/gsd-help`
|
||||
- Kilo:`/gsd-help`
|
||||
- Codex:`$gsd-help`
|
||||
- Copilot:`/gsd:help`
|
||||
- Antigravity:`/gsd:help`
|
||||
@@ -117,6 +118,10 @@ npx get-shit-done-cc --opencode --global # 安装到 ~/.config/opencode/
|
||||
# Gemini CLI
|
||||
npx get-shit-done-cc --gemini --global # 安装到 ~/.gemini/
|
||||
|
||||
# Kilo(OpenCode 分支)
|
||||
npx get-shit-done-cc --kilo --global # 安装到 ~/.config/kilo/
|
||||
npx get-shit-done-cc --kilo --local # 安装到 ./.kilo/
|
||||
|
||||
# Codex(以 skills 为主)
|
||||
npx get-shit-done-cc --codex --global # 安装到 ~/.codex/
|
||||
npx get-shit-done-cc --codex --local # 安装到 ./.codex/
|
||||
@@ -138,7 +143,7 @@ npx get-shit-done-cc --all --global # 安装到所有目录
|
||||
```
|
||||
|
||||
使用 `--global`(`-g`)或 `--local`(`-l`)可以跳过安装位置提示。
|
||||
使用 `--claude`、`--opencode`、`--gemini`、`--codex`、`--copilot`、`--cursor`、`--antigravity` 或 `--all` 可以跳过运行时提示。
|
||||
使用 `--claude`、`--opencode`、`--gemini`、`--kilo`、`--codex`、`--copilot`、`--cursor`、`--antigravity` 或 `--all` 可以跳过运行时提示。
|
||||
|
||||
</details>
|
||||
|
||||
@@ -743,6 +748,7 @@ CLAUDE_CONFIG_DIR=/home/youruser/.claude npx get-shit-done-cc --global
|
||||
npx get-shit-done-cc --claude --global --uninstall
|
||||
npx get-shit-done-cc --opencode --global --uninstall
|
||||
npx get-shit-done-cc --gemini --global --uninstall
|
||||
npx get-shit-done-cc --kilo --global --uninstall
|
||||
npx get-shit-done-cc --codex --global --uninstall
|
||||
npx get-shit-done-cc --copilot --global --uninstall
|
||||
npx get-shit-done-cc --cursor --global --uninstall
|
||||
@@ -752,6 +758,7 @@ npx get-shit-done-cc --antigravity --global --uninstall
|
||||
npx get-shit-done-cc --claude --local --uninstall
|
||||
npx get-shit-done-cc --opencode --local --uninstall
|
||||
npx get-shit-done-cc --gemini --local --uninstall
|
||||
npx get-shit-done-cc --kilo --local --uninstall
|
||||
npx get-shit-done-cc --codex --local --uninstall
|
||||
npx get-shit-done-cc --copilot --local --uninstall
|
||||
npx get-shit-done-cc --cursor --local --uninstall
|
||||
@@ -764,7 +771,7 @@ npx get-shit-done-cc --antigravity --local --uninstall
|
||||
|
||||
## 社区移植版本
|
||||
|
||||
OpenCode、Gemini CLI 和 Codex 现在都已经通过 `npx get-shit-done-cc` 获得原生支持。
|
||||
OpenCode、Gemini CLI、Kilo 和 Codex 现在都已经通过 `npx get-shit-done-cc` 获得原生支持。
|
||||
|
||||
这些社区移植版本曾率先探索多运行时支持:
|
||||
|
||||
|
||||
445
bin/install.js
445
bin/install.js
@@ -62,6 +62,7 @@ const hasLocal = args.includes('--local') || args.includes('-l');
|
||||
const hasOpencode = args.includes('--opencode');
|
||||
const hasClaude = args.includes('--claude');
|
||||
const hasGemini = args.includes('--gemini');
|
||||
const hasKilo = args.includes('--kilo');
|
||||
const hasCodex = args.includes('--codex');
|
||||
const hasCopilot = args.includes('--copilot');
|
||||
const hasAntigravity = args.includes('--antigravity');
|
||||
@@ -75,13 +76,14 @@ const hasUninstall = args.includes('--uninstall') || args.includes('-u');
|
||||
// Runtime selection - can be set by flags or interactive prompt
|
||||
let selectedRuntimes = [];
|
||||
if (hasAll) {
|
||||
selectedRuntimes = ['claude', 'opencode', 'gemini', 'codex', 'copilot', 'antigravity', 'cursor', 'windsurf'];
|
||||
selectedRuntimes = ['claude', 'opencode', 'gemini', 'kilo', 'codex', 'copilot', 'antigravity', 'cursor', 'windsurf'];
|
||||
} else if (hasBoth) {
|
||||
selectedRuntimes = ['claude', 'opencode'];
|
||||
} else {
|
||||
if (hasOpencode) selectedRuntimes.push('opencode');
|
||||
if (hasClaude) selectedRuntimes.push('claude');
|
||||
if (hasOpencode) selectedRuntimes.push('opencode');
|
||||
if (hasGemini) selectedRuntimes.push('gemini');
|
||||
if (hasKilo) selectedRuntimes.push('kilo');
|
||||
if (hasCodex) selectedRuntimes.push('codex');
|
||||
if (hasCopilot) selectedRuntimes.push('copilot');
|
||||
if (hasAntigravity) selectedRuntimes.push('antigravity');
|
||||
@@ -128,6 +130,7 @@ function getDirName(runtime) {
|
||||
if (runtime === 'copilot') return '.github';
|
||||
if (runtime === 'opencode') return '.opencode';
|
||||
if (runtime === 'gemini') return '.gemini';
|
||||
if (runtime === 'kilo') return '.kilo';
|
||||
if (runtime === 'codex') return '.codex';
|
||||
if (runtime === 'antigravity') return '.agent';
|
||||
if (runtime === 'cursor') return '.cursor';
|
||||
@@ -154,6 +157,7 @@ function getConfigDirFromHome(runtime, isGlobal) {
|
||||
return "'.config', 'opencode'";
|
||||
}
|
||||
if (runtime === 'gemini') return "'.gemini'";
|
||||
if (runtime === 'kilo') return "'.config', 'kilo'";
|
||||
if (runtime === 'codex') return "'.codex'";
|
||||
if (runtime === 'antigravity') {
|
||||
if (!isGlobal) return "'.agent'";
|
||||
@@ -189,6 +193,26 @@ function getOpencodeGlobalDir() {
|
||||
return path.join(os.homedir(), '.config', 'opencode');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the global config directory for Kilo
|
||||
* Kilo follows XDG Base Directory spec and uses ~/.config/kilo/
|
||||
* Priority: KILO_CONFIG_DIR > XDG_CONFIG_HOME/kilo > ~/.config/kilo
|
||||
*/
|
||||
function getKiloGlobalDir() {
|
||||
// 1. Explicit KILO_CONFIG_DIR env var
|
||||
if (process.env.KILO_CONFIG_DIR) {
|
||||
return expandTilde(process.env.KILO_CONFIG_DIR);
|
||||
}
|
||||
|
||||
// 2. XDG_CONFIG_HOME/kilo
|
||||
if (process.env.XDG_CONFIG_HOME) {
|
||||
return path.join(expandTilde(process.env.XDG_CONFIG_HOME), 'kilo');
|
||||
}
|
||||
|
||||
// 3. Default: ~/.config/kilo (XDG default)
|
||||
return path.join(os.homedir(), '.config', 'kilo');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the global config directory for a runtime
|
||||
* @param {string} runtime - 'claude', 'opencode', 'gemini', 'codex', or 'copilot'
|
||||
@@ -203,6 +227,14 @@ function getGlobalDir(runtime, explicitDir = null) {
|
||||
return getOpencodeGlobalDir();
|
||||
}
|
||||
|
||||
if (runtime === 'kilo') {
|
||||
// For Kilo, --config-dir overrides env vars
|
||||
if (explicitDir) {
|
||||
return expandTilde(explicitDir);
|
||||
}
|
||||
return getKiloGlobalDir();
|
||||
}
|
||||
|
||||
if (runtime === 'gemini') {
|
||||
// Gemini: --config-dir > GEMINI_CONFIG_DIR > ~/.gemini
|
||||
if (explicitDir) {
|
||||
@@ -290,7 +322,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, Codex, Copilot, Antigravity, Cursor, and Windsurf by TÂCHES.\n';
|
||||
' development system for Claude Code, OpenCode, Gemini, Kilo, Codex, Copilot, Antigravity, Cursor, and Windsurf by TÂCHES.\n';
|
||||
|
||||
// Parse --config-dir argument
|
||||
function parseConfigDirArg() {
|
||||
@@ -328,7 +360,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}--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}--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 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 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 --codex --global --config-dir ~/.codex-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 / GEMINI_CONFIG_DIR / CODEX_HOME / COPILOT_CONFIG_DIR / ANTIGRAVITY_CONFIG_DIR / CURSOR_CONFIG_DIR / WINDSURF_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}--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 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 / GEMINI_CONFIG_DIR / KILO_CONFIG_DIR / CODEX_HOME / COPILOT_CONFIG_DIR / ANTIGRAVITY_CONFIG_DIR / CURSOR_CONFIG_DIR / WINDSURF_CONFIG_DIR environment variables.\n`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
@@ -363,6 +395,17 @@ function resolveOpencodeConfigPath(configDir) {
|
||||
return path.join(configDir, 'opencode.json');
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the Kilo config file path, preferring .jsonc if it exists.
|
||||
*/
|
||||
function resolveKiloConfigPath(configDir) {
|
||||
const jsoncPath = path.join(configDir, 'kilo.jsonc');
|
||||
if (fs.existsSync(jsoncPath)) {
|
||||
return jsoncPath;
|
||||
}
|
||||
return path.join(configDir, 'kilo.json');
|
||||
}
|
||||
|
||||
/**
|
||||
* Read and parse settings.json, returning empty object if it doesn't exist
|
||||
*/
|
||||
@@ -377,6 +420,20 @@ function readSettings(settingsPath) {
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Read and parse JSONC config, returning empty object if it doesn't exist
|
||||
*/
|
||||
function readJsoncSettings(settingsPath) {
|
||||
if (fs.existsSync(settingsPath)) {
|
||||
try {
|
||||
return parseJsonc(fs.readFileSync(settingsPath, 'utf8'));
|
||||
} catch (e) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Write settings.json with proper formatting
|
||||
*/
|
||||
@@ -400,8 +457,11 @@ function getCommitAttribution(runtime) {
|
||||
|
||||
let result;
|
||||
|
||||
if (runtime === 'opencode') {
|
||||
const config = readSettings(resolveOpencodeConfigPath(getGlobalDir('opencode', null)));
|
||||
if (runtime === 'opencode' || runtime === 'kilo') {
|
||||
const resolveConfigPath = runtime === 'opencode'
|
||||
? resolveOpencodeConfigPath
|
||||
: resolveKiloConfigPath;
|
||||
const config = readJsoncSettings(resolveConfigPath(getGlobalDir(runtime, null)));
|
||||
result = config.disable_ai_attribution === true ? null : undefined;
|
||||
} else if (runtime === 'gemini') {
|
||||
// Gemini: check gemini settings.json for attribution config
|
||||
@@ -2649,6 +2709,154 @@ function convertClaudeToOpencodeFrontmatter(content, { isAgent = false } = {}) {
|
||||
return `---\n${newFrontmatter}\n---${body}`;
|
||||
}
|
||||
|
||||
// Kilo CLI — fork of OpenCode, same conversion logic, different config paths.
|
||||
function convertClaudeToKiloFrontmatter(content, { isAgent = false } = {}) {
|
||||
// Replace tool name references in content (applies to all files)
|
||||
let convertedContent = content;
|
||||
convertedContent = convertedContent.replace(/\bAskUserQuestion\b/g, 'question');
|
||||
convertedContent = convertedContent.replace(/\bSlashCommand\b/g, 'skill');
|
||||
convertedContent = convertedContent.replace(/\bTodoWrite\b/g, 'todowrite');
|
||||
// Replace /gsd:command with /gsd-command for Kilo (flat command structure)
|
||||
convertedContent = convertedContent.replace(/\/gsd:/g, '/gsd-');
|
||||
// Replace ~/.claude and $HOME/.claude with Kilo's config location
|
||||
convertedContent = convertedContent.replace(/~\/\.claude\b/g, '~/.config/kilo');
|
||||
convertedContent = convertedContent.replace(/\$HOME\/\.claude\b/g, '$HOME/.config/kilo');
|
||||
// Replace general-purpose subagent type with Kilo's equivalent "general"
|
||||
convertedContent = convertedContent.replace(/subagent_type="general-purpose"/g, 'subagent_type="general"');
|
||||
// Runtime-neutral agent name replacement (#766)
|
||||
convertedContent = neutralizeAgentReferences(convertedContent, 'AGENTS.md');
|
||||
|
||||
// Check if content has frontmatter
|
||||
if (!convertedContent.startsWith('---')) {
|
||||
return convertedContent;
|
||||
}
|
||||
|
||||
// Find the end of frontmatter
|
||||
const endIndex = convertedContent.indexOf('---', 3);
|
||||
if (endIndex === -1) {
|
||||
return convertedContent;
|
||||
}
|
||||
|
||||
const frontmatter = convertedContent.substring(3, endIndex).trim();
|
||||
const body = convertedContent.substring(endIndex + 3);
|
||||
|
||||
// Parse frontmatter line by line (simple YAML parsing)
|
||||
const lines = frontmatter.split('\n');
|
||||
const newLines = [];
|
||||
let inAllowedTools = false;
|
||||
let inSkippedArray = false;
|
||||
const allowedTools = [];
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
|
||||
// For agents: skip commented-out lines (e.g. hooks blocks)
|
||||
if (isAgent && trimmed.startsWith('#')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Detect start of allowed-tools array
|
||||
if (trimmed.startsWith('allowed-tools:')) {
|
||||
inAllowedTools = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Detect inline tools: field (comma-separated string)
|
||||
if (trimmed.startsWith('tools:')) {
|
||||
if (isAgent) {
|
||||
// Agents: strip tools entirely (not supported in Kilo agent frontmatter)
|
||||
inSkippedArray = true;
|
||||
continue;
|
||||
}
|
||||
const toolsValue = trimmed.substring(6).trim();
|
||||
if (toolsValue) {
|
||||
// Parse comma-separated tools
|
||||
const tools = toolsValue.split(',').map(t => t.trim()).filter(t => t);
|
||||
allowedTools.push(...tools);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// For agents: strip skills:, color:, memory:, maxTurns:, permissionMode:, disallowedTools:
|
||||
if (isAgent && /^(skills|color|memory|maxTurns|permissionMode|disallowedTools):/.test(trimmed)) {
|
||||
inSkippedArray = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip continuation lines of a stripped array/object field
|
||||
if (inSkippedArray) {
|
||||
if (trimmed.startsWith('- ') || trimmed.startsWith('#') || /^\s/.test(line)) {
|
||||
continue;
|
||||
}
|
||||
inSkippedArray = false;
|
||||
}
|
||||
|
||||
// For commands: remove name: field (Kilo uses filename for command name)
|
||||
// For agents: keep name: (required by Kilo agents)
|
||||
if (!isAgent && trimmed.startsWith('name:')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Strip model: field — Kilo doesn't support Claude Code model aliases
|
||||
// like 'haiku', 'sonnet', 'opus', or 'inherit'. Omitting lets Kilo use
|
||||
// its configured default model.
|
||||
if (trimmed.startsWith('model:')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Convert color names to hex for Kilo (commands only; agents strip color above)
|
||||
if (trimmed.startsWith('color:')) {
|
||||
const colorValue = trimmed.substring(6).trim().toLowerCase();
|
||||
const hexColor = colorNameToHex[colorValue];
|
||||
if (hexColor) {
|
||||
newLines.push(`color: "${hexColor}"`);
|
||||
} else if (colorValue.startsWith('#')) {
|
||||
// Validate hex color format (#RGB or #RRGGBB)
|
||||
if (/^#[0-9a-f]{3}$|^#[0-9a-f]{6}$/i.test(colorValue)) {
|
||||
// Already hex and valid, keep as is
|
||||
newLines.push(line);
|
||||
}
|
||||
// Skip invalid hex colors
|
||||
}
|
||||
// Skip unknown color names
|
||||
continue;
|
||||
}
|
||||
|
||||
// Collect allowed-tools items
|
||||
if (inAllowedTools) {
|
||||
if (trimmed.startsWith('- ')) {
|
||||
allowedTools.push(trimmed.substring(2).trim());
|
||||
continue;
|
||||
} else if (trimmed && !trimmed.startsWith('-')) {
|
||||
// End of array, new field started
|
||||
inAllowedTools = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Keep other fields
|
||||
if (!inAllowedTools) {
|
||||
newLines.push(line);
|
||||
}
|
||||
}
|
||||
|
||||
// For agents: add required Kilo agent fields
|
||||
if (isAgent) {
|
||||
newLines.push('mode: subagent');
|
||||
}
|
||||
|
||||
// For commands: add tools object if we had allowed-tools or tools
|
||||
if (!isAgent && allowedTools.length > 0) {
|
||||
newLines.push('tools:');
|
||||
for (const tool of allowedTools) {
|
||||
newLines.push(` ${convertToolName(tool)}: true`);
|
||||
}
|
||||
}
|
||||
|
||||
// Rebuild frontmatter (body already has tool names converted)
|
||||
const newFrontmatter = newLines.join('\n').trim();
|
||||
return `---\n${newFrontmatter}\n---${body}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert Claude Code markdown command to Gemini TOML format
|
||||
* @param {string} content - Markdown file content with YAML frontmatter
|
||||
@@ -2699,7 +2907,7 @@ function convertClaudeToGeminiToml(content) {
|
||||
* @param {string} destDir - Destination directory (e.g., command/)
|
||||
* @param {string} prefix - Prefix for filenames (e.g., 'gsd')
|
||||
* @param {string} pathPrefix - Path prefix for file references
|
||||
* @param {string} runtime - Target runtime ('claude' or 'opencode')
|
||||
* @param {string} runtime - Target runtime ('claude', 'opencode', or 'kilo')
|
||||
*/
|
||||
function copyFlattenedCommands(srcDir, destDir, prefix, pathPrefix, runtime) {
|
||||
if (!fs.existsSync(srcDir)) {
|
||||
@@ -2742,7 +2950,9 @@ function copyFlattenedCommands(srcDir, destDir, prefix, pathPrefix, runtime) {
|
||||
content = content.replace(localClaudeRegex, `./${getDirName(runtime)}/`);
|
||||
content = content.replace(opencodeDirRegex, pathPrefix);
|
||||
content = processAttribution(content, getCommitAttribution(runtime));
|
||||
content = convertClaudeToOpencodeFrontmatter(content);
|
||||
content = runtime === 'kilo'
|
||||
? convertClaudeToKiloFrontmatter(content)
|
||||
: convertClaudeToOpencodeFrontmatter(content);
|
||||
|
||||
fs.writeFileSync(destPath, content);
|
||||
}
|
||||
@@ -3035,6 +3245,7 @@ function copyCommandsAsAntigravitySkills(srcDir, skillsDir, prefix, isGlobal = f
|
||||
*/
|
||||
function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime, isCommand = false, isGlobal = false) {
|
||||
const isOpencode = runtime === 'opencode';
|
||||
const isKilo = runtime === 'kilo';
|
||||
const isCodex = runtime === 'codex';
|
||||
const isCopilot = runtime === 'copilot';
|
||||
const isAntigravity = runtime === 'antigravity';
|
||||
@@ -3071,8 +3282,10 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime, isCommand
|
||||
content = processAttribution(content, getCommitAttribution(runtime));
|
||||
|
||||
// Convert frontmatter for opencode compatibility
|
||||
if (isOpencode) {
|
||||
content = convertClaudeToOpencodeFrontmatter(content);
|
||||
if (isOpencode || isKilo) {
|
||||
content = isKilo
|
||||
? convertClaudeToKiloFrontmatter(content)
|
||||
: convertClaudeToOpencodeFrontmatter(content);
|
||||
fs.writeFileSync(destPath, content);
|
||||
} else if (runtime === 'gemini') {
|
||||
if (isCommand) {
|
||||
@@ -3299,6 +3512,7 @@ function validateHookFields(settings) {
|
||||
*/
|
||||
function uninstall(isGlobal, runtime = 'claude') {
|
||||
const isOpencode = runtime === 'opencode';
|
||||
const isKilo = runtime === 'kilo';
|
||||
const isCodex = runtime === 'codex';
|
||||
const isCopilot = runtime === 'copilot';
|
||||
const isAntigravity = runtime === 'antigravity';
|
||||
@@ -3318,6 +3532,7 @@ function uninstall(isGlobal, runtime = 'claude') {
|
||||
let runtimeLabel = 'Claude Code';
|
||||
if (runtime === 'opencode') runtimeLabel = 'OpenCode';
|
||||
if (runtime === 'gemini') runtimeLabel = 'Gemini';
|
||||
if (runtime === 'kilo') runtimeLabel = 'Kilo';
|
||||
if (runtime === 'codex') runtimeLabel = 'Codex';
|
||||
if (runtime === 'copilot') runtimeLabel = 'Copilot';
|
||||
if (runtime === 'antigravity') runtimeLabel = 'Antigravity';
|
||||
@@ -3336,8 +3551,8 @@ function uninstall(isGlobal, runtime = 'claude') {
|
||||
let removedCount = 0;
|
||||
|
||||
// 1. Remove GSD commands/skills
|
||||
if (isOpencode) {
|
||||
// OpenCode: remove command/gsd-*.md files
|
||||
if (isOpencode || isKilo) {
|
||||
// OpenCode/Kilo: remove command/gsd-*.md files
|
||||
const commandDir = path.join(targetDir, 'command');
|
||||
if (fs.existsSync(commandDir)) {
|
||||
const files = fs.readdirSync(commandDir);
|
||||
@@ -3650,10 +3865,48 @@ function uninstall(isGlobal, runtime = 'claude') {
|
||||
|
||||
// 6. For OpenCode, clean up permissions from opencode.json or opencode.jsonc
|
||||
if (isOpencode) {
|
||||
const opencodeConfigDir = isGlobal
|
||||
? getOpencodeGlobalDir()
|
||||
: path.join(process.cwd(), '.opencode');
|
||||
const configPath = resolveOpencodeConfigPath(opencodeConfigDir);
|
||||
const configPath = resolveOpencodeConfigPath(targetDir);
|
||||
if (fs.existsSync(configPath)) {
|
||||
try {
|
||||
const config = parseJsonc(fs.readFileSync(configPath, 'utf8'));
|
||||
let modified = false;
|
||||
|
||||
// Remove GSD permission entries
|
||||
if (config.permission) {
|
||||
for (const permType of ['read', 'external_directory']) {
|
||||
if (config.permission[permType]) {
|
||||
const keys = Object.keys(config.permission[permType]);
|
||||
for (const key of keys) {
|
||||
if (key.includes('get-shit-done')) {
|
||||
delete config.permission[permType][key];
|
||||
modified = true;
|
||||
}
|
||||
}
|
||||
// Clean up empty objects
|
||||
if (Object.keys(config.permission[permType]).length === 0) {
|
||||
delete config.permission[permType];
|
||||
}
|
||||
}
|
||||
}
|
||||
if (Object.keys(config.permission).length === 0) {
|
||||
delete config.permission;
|
||||
}
|
||||
}
|
||||
|
||||
if (modified) {
|
||||
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
|
||||
removedCount++;
|
||||
console.log(` ${green}✓${reset} Removed GSD permissions from ${path.basename(configPath)}`);
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore JSON parse errors
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 7. For Kilo, clean up permissions from kilo.json or kilo.jsonc
|
||||
if (isKilo) {
|
||||
const configPath = resolveKiloConfigPath(targetDir);
|
||||
if (fs.existsSync(configPath)) {
|
||||
try {
|
||||
const config = parseJsonc(fs.readFileSync(configPath, 'utf8'));
|
||||
@@ -3767,13 +4020,14 @@ function parseJsonc(content) {
|
||||
* Configure OpenCode permissions to allow reading GSD reference docs
|
||||
* This prevents permission prompts when GSD accesses the get-shit-done directory
|
||||
* @param {boolean} isGlobal - Whether this is a global or local install
|
||||
* @param {string|null} configDir - Resolved config directory when already known
|
||||
*/
|
||||
function configureOpencodePermissions(isGlobal = true) {
|
||||
function configureOpencodePermissions(isGlobal = true, configDir = null) {
|
||||
// For local installs, use ./.opencode/
|
||||
// For global installs, use ~/.config/opencode/
|
||||
const opencodeConfigDir = isGlobal
|
||||
? getOpencodeGlobalDir()
|
||||
: path.join(process.cwd(), '.opencode');
|
||||
const opencodeConfigDir = configDir || (isGlobal
|
||||
? getGlobalDir('opencode', explicitConfigDir)
|
||||
: path.join(process.cwd(), '.opencode'));
|
||||
// Ensure config directory exists
|
||||
fs.mkdirSync(opencodeConfigDir, { recursive: true });
|
||||
|
||||
@@ -3836,6 +4090,80 @@ function configureOpencodePermissions(isGlobal = true) {
|
||||
console.log(` ${green}✓${reset} Configured read permission for GSD docs`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure Kilo permissions to allow reading GSD reference docs
|
||||
* This prevents permission prompts when GSD accesses the get-shit-done directory
|
||||
* @param {boolean} isGlobal - Whether this is a global or local install
|
||||
* @param {string|null} configDir - Resolved config directory when already known
|
||||
*/
|
||||
function configureKiloPermissions(isGlobal = true, configDir = null) {
|
||||
// For local installs, use ./.kilo/
|
||||
// For global installs, use ~/.config/kilo/
|
||||
const kiloConfigDir = configDir || (isGlobal
|
||||
? getGlobalDir('kilo', explicitConfigDir)
|
||||
: path.join(process.cwd(), '.kilo'));
|
||||
// Ensure config directory exists
|
||||
fs.mkdirSync(kiloConfigDir, { recursive: true });
|
||||
|
||||
const configPath = resolveKiloConfigPath(kiloConfigDir);
|
||||
|
||||
// Read existing config or create empty object
|
||||
let config = {};
|
||||
if (fs.existsSync(configPath)) {
|
||||
try {
|
||||
const content = fs.readFileSync(configPath, 'utf8');
|
||||
config = parseJsonc(content);
|
||||
} catch (e) {
|
||||
// Cannot parse - DO NOT overwrite user's config
|
||||
const configFile = path.basename(configPath);
|
||||
console.log(` ${yellow}⚠${reset} Could not parse ${configFile} - skipping permission config`);
|
||||
console.log(` ${dim}Reason: ${e.message}${reset}`);
|
||||
console.log(` ${dim}Your config was NOT modified. Fix the syntax manually if needed.${reset}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure permission structure exists
|
||||
if (!config.permission) {
|
||||
config.permission = {};
|
||||
}
|
||||
|
||||
// Build the GSD path using the actual config directory
|
||||
// Use ~ shorthand if it's in the default location, otherwise use full path
|
||||
const defaultConfigDir = path.join(os.homedir(), '.config', 'kilo');
|
||||
const gsdPath = kiloConfigDir === defaultConfigDir
|
||||
? '~/.config/kilo/get-shit-done/*'
|
||||
: `${kiloConfigDir.replace(/\\/g, '/')}/get-shit-done/*`;
|
||||
|
||||
let modified = false;
|
||||
|
||||
// Configure read permission
|
||||
if (!config.permission.read || typeof config.permission.read !== 'object') {
|
||||
config.permission.read = {};
|
||||
}
|
||||
if (config.permission.read[gsdPath] !== 'allow') {
|
||||
config.permission.read[gsdPath] = 'allow';
|
||||
modified = true;
|
||||
}
|
||||
|
||||
// Configure external_directory permission (the safety guard for paths outside project)
|
||||
if (!config.permission.external_directory || typeof config.permission.external_directory !== 'object') {
|
||||
config.permission.external_directory = {};
|
||||
}
|
||||
if (config.permission.external_directory[gsdPath] !== 'allow') {
|
||||
config.permission.external_directory[gsdPath] = 'allow';
|
||||
modified = true;
|
||||
}
|
||||
|
||||
if (!modified) {
|
||||
return; // Already configured
|
||||
}
|
||||
|
||||
// Write config back
|
||||
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
|
||||
console.log(` ${green}✓${reset} Configured read permission for GSD docs`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify a directory exists and contains files
|
||||
*/
|
||||
@@ -3914,6 +4242,7 @@ function generateManifest(dir, baseDir) {
|
||||
*/
|
||||
function writeManifest(configDir, runtime = 'claude') {
|
||||
const isOpencode = runtime === 'opencode';
|
||||
const isKilo = runtime === 'kilo';
|
||||
const isCodex = runtime === 'codex';
|
||||
const isCopilot = runtime === 'copilot';
|
||||
const isAntigravity = runtime === 'antigravity';
|
||||
@@ -3936,7 +4265,7 @@ function writeManifest(configDir, runtime = 'claude') {
|
||||
manifest.files['commands/gsd/' + rel] = hash;
|
||||
}
|
||||
}
|
||||
if (isOpencode && fs.existsSync(opencodeCommandDir)) {
|
||||
if ((isOpencode || isKilo) && fs.existsSync(opencodeCommandDir)) {
|
||||
for (const file of fs.readdirSync(opencodeCommandDir)) {
|
||||
if (file.startsWith('gsd-') && file.endsWith('.md')) {
|
||||
manifest.files['command/' + file] = fileHash(path.join(opencodeCommandDir, file));
|
||||
@@ -4029,7 +4358,7 @@ function reportLocalPatches(configDir, runtime = 'claude') {
|
||||
try { meta = JSON.parse(fs.readFileSync(metaPath, 'utf8')); } catch { return []; }
|
||||
|
||||
if (meta.files && meta.files.length > 0) {
|
||||
const reapplyCommand = (runtime === 'opencode' || runtime === 'copilot')
|
||||
const reapplyCommand = (runtime === 'opencode' || runtime === 'kilo' || runtime === 'copilot')
|
||||
? '/gsd-reapply-patches'
|
||||
: runtime === 'codex'
|
||||
? '$gsd-reapply-patches'
|
||||
@@ -4053,6 +4382,7 @@ function reportLocalPatches(configDir, runtime = 'claude') {
|
||||
function install(isGlobal, runtime = 'claude') {
|
||||
const isOpencode = runtime === 'opencode';
|
||||
const isGemini = runtime === 'gemini';
|
||||
const isKilo = runtime === 'kilo';
|
||||
const isCodex = runtime === 'codex';
|
||||
const isCopilot = runtime === 'copilot';
|
||||
const isAntigravity = runtime === 'antigravity';
|
||||
@@ -4084,6 +4414,7 @@ function install(isGlobal, runtime = 'claude') {
|
||||
let runtimeLabel = 'Claude Code';
|
||||
if (isOpencode) runtimeLabel = 'OpenCode';
|
||||
if (isGemini) runtimeLabel = 'Gemini';
|
||||
if (isKilo) runtimeLabel = 'Kilo';
|
||||
if (isCodex) runtimeLabel = 'Codex';
|
||||
if (isCopilot) runtimeLabel = 'Copilot';
|
||||
if (isAntigravity) runtimeLabel = 'Antigravity';
|
||||
@@ -4101,9 +4432,9 @@ function install(isGlobal, runtime = 'claude') {
|
||||
// Clean up orphaned files from previous versions
|
||||
cleanupOrphanedFiles(targetDir);
|
||||
|
||||
// OpenCode uses command/ (flat), Codex uses skills/, Claude/Gemini use commands/gsd/
|
||||
if (isOpencode) {
|
||||
// OpenCode: flat structure in command/ directory
|
||||
// OpenCode/Kilo use command/ (flat), Codex uses skills/, Claude/Gemini use commands/gsd/
|
||||
if (isOpencode || isKilo) {
|
||||
// OpenCode/Kilo: flat structure in command/ directory
|
||||
const commandDir = path.join(targetDir, 'command');
|
||||
fs.mkdirSync(commandDir, { recursive: true });
|
||||
|
||||
@@ -4232,6 +4563,8 @@ function install(isGlobal, runtime = 'claude') {
|
||||
// Convert frontmatter for runtime compatibility (agents need different handling)
|
||||
if (isOpencode) {
|
||||
content = convertClaudeToOpencodeFrontmatter(content, { isAgent: true });
|
||||
} else if (isKilo) {
|
||||
content = convertClaudeToKiloFrontmatter(content, { isAgent: true });
|
||||
} else if (isGemini) {
|
||||
content = convertClaudeToGeminiAgent(content);
|
||||
} else if (isCodex) {
|
||||
@@ -4417,7 +4750,7 @@ function install(isGlobal, runtime = 'claude') {
|
||||
console.warn(` ${yellow}⚠${reset} Could not configure Codex hooks: ${e.message}`);
|
||||
}
|
||||
|
||||
return { settingsPath: null, settings: null, statuslineCommand: null, runtime };
|
||||
return { settingsPath: null, settings: null, statuslineCommand: null, runtime, configDir: targetDir };
|
||||
}
|
||||
|
||||
if (isCopilot) {
|
||||
@@ -4430,17 +4763,17 @@ function install(isGlobal, runtime = 'claude') {
|
||||
console.log(` ${green}✓${reset} Generated copilot-instructions.md`);
|
||||
}
|
||||
// Copilot: no settings.json, no hooks, no statusline (like Codex)
|
||||
return { settingsPath: null, settings: null, statuslineCommand: null, runtime };
|
||||
return { settingsPath: null, settings: null, statuslineCommand: null, runtime, configDir: targetDir };
|
||||
}
|
||||
|
||||
if (isCursor) {
|
||||
// Cursor uses skills — no config.toml, no settings.json hooks needed
|
||||
return { settingsPath: null, settings: null, statuslineCommand: null, runtime };
|
||||
return { settingsPath: null, settings: null, statuslineCommand: null, runtime, configDir: targetDir };
|
||||
}
|
||||
|
||||
if (isWindsurf) {
|
||||
// Windsurf uses skills — no config.toml, no settings.json hooks needed
|
||||
return { settingsPath: null, settings: null, statuslineCommand: null, runtime };
|
||||
return { settingsPath: null, settings: null, statuslineCommand: null, runtime, configDir: targetDir };
|
||||
}
|
||||
|
||||
// Configure statusline and hooks in settings.json
|
||||
@@ -4473,7 +4806,7 @@ function install(isGlobal, runtime = 'claude') {
|
||||
}
|
||||
|
||||
// Configure SessionStart hook for update checking (skip for opencode)
|
||||
if (!isOpencode) {
|
||||
if (!isOpencode && !isKilo) {
|
||||
if (!settings.hooks) {
|
||||
settings.hooks = {};
|
||||
}
|
||||
@@ -4566,20 +4899,21 @@ function install(isGlobal, runtime = 'claude') {
|
||||
}
|
||||
}
|
||||
|
||||
return { settingsPath, settings, statuslineCommand, runtime };
|
||||
return { settingsPath, settings, statuslineCommand, runtime, configDir: targetDir };
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply statusline config, then print completion message
|
||||
*/
|
||||
function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallStatusline, runtime = 'claude', isGlobal = true) {
|
||||
function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallStatusline, runtime = 'claude', isGlobal = true, configDir = null) {
|
||||
const isOpencode = runtime === 'opencode';
|
||||
const isKilo = runtime === 'kilo';
|
||||
const isCodex = runtime === 'codex';
|
||||
const isCopilot = runtime === 'copilot';
|
||||
const isCursor = runtime === 'cursor';
|
||||
const isWindsurf = runtime === 'windsurf';
|
||||
|
||||
if (shouldInstallStatusline && !isOpencode && !isCodex && !isCopilot && !isCursor && !isWindsurf) {
|
||||
if (shouldInstallStatusline && !isOpencode && !isKilo && !isCodex && !isCopilot && !isCursor && !isWindsurf) {
|
||||
settings.statusLine = {
|
||||
type: 'command',
|
||||
command: statuslineCommand
|
||||
@@ -4588,13 +4922,18 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
|
||||
}
|
||||
|
||||
// Write settings when runtime supports settings.json
|
||||
if (!isCodex && !isCopilot && !isCursor && !isWindsurf) {
|
||||
if (!isCodex && !isCopilot && !isKilo && !isCursor && !isWindsurf) {
|
||||
writeSettings(settingsPath, settings);
|
||||
}
|
||||
|
||||
// Configure OpenCode permissions
|
||||
if (isOpencode) {
|
||||
configureOpencodePermissions(isGlobal);
|
||||
configureOpencodePermissions(isGlobal, configDir);
|
||||
}
|
||||
|
||||
// Configure Kilo permissions
|
||||
if (isKilo) {
|
||||
configureKiloPermissions(isGlobal, configDir);
|
||||
}
|
||||
|
||||
// For non-Claude runtimes, set resolve_model_ids: "omit" in ~/.gsd/defaults.json
|
||||
@@ -4621,6 +4960,7 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
|
||||
let program = 'Claude Code';
|
||||
if (runtime === 'opencode') program = 'OpenCode';
|
||||
if (runtime === 'gemini') program = 'Gemini';
|
||||
if (runtime === 'kilo') program = 'Kilo';
|
||||
if (runtime === 'codex') program = 'Codex';
|
||||
if (runtime === 'copilot') program = 'Copilot';
|
||||
if (runtime === 'antigravity') program = 'Antigravity';
|
||||
@@ -4628,6 +4968,7 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
|
||||
|
||||
let command = '/gsd:new-project';
|
||||
if (runtime === 'opencode') command = '/gsd-new-project';
|
||||
if (runtime === 'kilo') command = '/gsd-new-project';
|
||||
if (runtime === 'codex') command = '$gsd-new-project';
|
||||
if (runtime === 'copilot') command = '/gsd-new-project';
|
||||
if (runtime === 'antigravity') command = '/gsd-new-project';
|
||||
@@ -4778,23 +5119,25 @@ function promptRuntime(callback) {
|
||||
'1': 'claude',
|
||||
'2': 'opencode',
|
||||
'3': 'gemini',
|
||||
'4': 'codex',
|
||||
'5': 'copilot',
|
||||
'6': 'antigravity',
|
||||
'7': 'cursor',
|
||||
'8': 'windsurf'
|
||||
'4': 'kilo',
|
||||
'5': 'codex',
|
||||
'6': 'copilot',
|
||||
'7': 'antigravity',
|
||||
'8': 'cursor',
|
||||
'9': 'windsurf'
|
||||
};
|
||||
const allRuntimes = ['claude', 'opencode', 'gemini', 'codex', 'copilot', 'antigravity', 'cursor', 'windsurf'];
|
||||
const allRuntimes = ['claude', 'opencode', 'gemini', 'kilo', 'codex', 'copilot', 'antigravity', 'cursor', 'windsurf'];
|
||||
|
||||
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}) OpenCode ${dim}(~/.config/opencode)${reset} - open source, free models
|
||||
${cyan}3${reset}) Gemini ${dim}(~/.gemini)${reset}
|
||||
${cyan}4${reset}) Codex ${dim}(~/.codex)${reset}
|
||||
${cyan}5${reset}) Copilot ${dim}(~/.copilot)${reset}
|
||||
${cyan}6${reset}) Antigravity ${dim}(~/.gemini/antigravity)${reset}
|
||||
${cyan}7${reset}) Cursor ${dim}(~/.cursor)${reset}
|
||||
${cyan}8${reset}) Windsurf ${dim}(~/.windsurf)${reset}
|
||||
${cyan}9${reset}) All
|
||||
${cyan}4${reset}) Kilo ${dim}(~/.config/kilo)${reset} - OpenCode fork
|
||||
${cyan}5${reset}) Codex ${dim}(~/.codex)${reset}
|
||||
${cyan}6${reset}) Copilot ${dim}(~/.copilot)${reset}
|
||||
${cyan}7${reset}) Antigravity ${dim}(~/.gemini/antigravity)${reset}
|
||||
${cyan}8${reset}) Cursor ${dim}(~/.cursor)${reset}
|
||||
${cyan}9${reset}) Windsurf ${dim}(~/.windsurf)${reset}
|
||||
${cyan}10${reset}) All
|
||||
|
||||
${dim}Select multiple: 1,4,6 or 1 4 6${reset}
|
||||
`);
|
||||
@@ -4805,7 +5148,7 @@ function promptRuntime(callback) {
|
||||
const input = answer.trim() || '1';
|
||||
|
||||
// "All" shortcut
|
||||
if (input === '9') {
|
||||
if (input === '10') {
|
||||
callback(allRuntimes);
|
||||
return;
|
||||
}
|
||||
@@ -4894,7 +5237,8 @@ function installAllRuntimes(runtimes, isGlobal, isInteractive) {
|
||||
result.statuslineCommand,
|
||||
useStatusline,
|
||||
result.runtime,
|
||||
isGlobal
|
||||
isGlobal,
|
||||
result.configDir
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -4937,12 +5281,15 @@ if (process.env.GSD_TEST_MODE) {
|
||||
install,
|
||||
convertClaudeCommandToCodexSkill,
|
||||
convertClaudeToOpencodeFrontmatter,
|
||||
convertClaudeToKiloFrontmatter,
|
||||
neutralizeAgentReferences,
|
||||
GSD_CODEX_MARKER,
|
||||
CODEX_AGENT_SANDBOX,
|
||||
getDirName,
|
||||
getGlobalDir,
|
||||
getConfigDirFromHome,
|
||||
resolveKiloConfigPath,
|
||||
configureKiloPermissions,
|
||||
claudeToCopilotTools,
|
||||
convertCopilotToolName,
|
||||
convertClaudeToCopilotContent,
|
||||
|
||||
@@ -14,19 +14,85 @@ After a GSD update wipes and reinstalls files, this command merges user's previo
|
||||
Check for local patches directory:
|
||||
|
||||
```bash
|
||||
# Global install — detect runtime config directory
|
||||
if [ -d "$HOME/.config/opencode/gsd-local-patches" ]; then
|
||||
PATCHES_DIR="$HOME/.config/opencode/gsd-local-patches"
|
||||
elif [ -d "$HOME/.opencode/gsd-local-patches" ]; then
|
||||
PATCHES_DIR="$HOME/.opencode/gsd-local-patches"
|
||||
elif [ -d "$HOME/.gemini/gsd-local-patches" ]; then
|
||||
PATCHES_DIR="$HOME/.gemini/gsd-local-patches"
|
||||
else
|
||||
PATCHES_DIR="$HOME/.claude/gsd-local-patches"
|
||||
expand_home() {
|
||||
case "$1" in
|
||||
"~/"*) printf '%s/%s\n' "$HOME" "${1#~/}" ;;
|
||||
*) printf '%s\n' "$1" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
PATCHES_DIR=""
|
||||
|
||||
# Env overrides first — covers custom config directories used with --config-dir
|
||||
if [ -n "$KILO_CONFIG_DIR" ]; then
|
||||
candidate="$(expand_home "$KILO_CONFIG_DIR")/gsd-local-patches"
|
||||
if [ -d "$candidate" ]; then
|
||||
PATCHES_DIR="$candidate"
|
||||
fi
|
||||
elif [ -n "$XDG_CONFIG_HOME" ]; then
|
||||
candidate="$(expand_home "$XDG_CONFIG_HOME")/kilo/gsd-local-patches"
|
||||
if [ -d "$candidate" ]; then
|
||||
PATCHES_DIR="$candidate"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$PATCHES_DIR" ] && [ -n "$OPENCODE_CONFIG_DIR" ]; then
|
||||
candidate="$(expand_home "$OPENCODE_CONFIG_DIR")/gsd-local-patches"
|
||||
if [ -d "$candidate" ]; then
|
||||
PATCHES_DIR="$candidate"
|
||||
fi
|
||||
elif [ -z "$PATCHES_DIR" ] && [ -n "$OPENCODE_CONFIG" ]; then
|
||||
candidate="$(dirname "$(expand_home "$OPENCODE_CONFIG")")/gsd-local-patches"
|
||||
if [ -d "$candidate" ]; then
|
||||
PATCHES_DIR="$candidate"
|
||||
fi
|
||||
elif [ -z "$PATCHES_DIR" ] && [ -n "$XDG_CONFIG_HOME" ]; then
|
||||
candidate="$(expand_home "$XDG_CONFIG_HOME")/opencode/gsd-local-patches"
|
||||
if [ -d "$candidate" ]; then
|
||||
PATCHES_DIR="$candidate"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$PATCHES_DIR" ] && [ -n "$GEMINI_CONFIG_DIR" ]; then
|
||||
candidate="$(expand_home "$GEMINI_CONFIG_DIR")/gsd-local-patches"
|
||||
if [ -d "$candidate" ]; then
|
||||
PATCHES_DIR="$candidate"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$PATCHES_DIR" ] && [ -n "$CODEX_HOME" ]; then
|
||||
candidate="$(expand_home "$CODEX_HOME")/gsd-local-patches"
|
||||
if [ -d "$candidate" ]; then
|
||||
PATCHES_DIR="$candidate"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$PATCHES_DIR" ] && [ -n "$CLAUDE_CONFIG_DIR" ]; then
|
||||
candidate="$(expand_home "$CLAUDE_CONFIG_DIR")/gsd-local-patches"
|
||||
if [ -d "$candidate" ]; then
|
||||
PATCHES_DIR="$candidate"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Global install — detect runtime config directory defaults
|
||||
if [ -z "$PATCHES_DIR" ]; then
|
||||
if [ -d "$HOME/.config/kilo/gsd-local-patches" ]; then
|
||||
PATCHES_DIR="$HOME/.config/kilo/gsd-local-patches"
|
||||
elif [ -d "$HOME/.config/opencode/gsd-local-patches" ]; then
|
||||
PATCHES_DIR="$HOME/.config/opencode/gsd-local-patches"
|
||||
elif [ -d "$HOME/.opencode/gsd-local-patches" ]; then
|
||||
PATCHES_DIR="$HOME/.opencode/gsd-local-patches"
|
||||
elif [ -d "$HOME/.gemini/gsd-local-patches" ]; then
|
||||
PATCHES_DIR="$HOME/.gemini/gsd-local-patches"
|
||||
elif [ -d "$HOME/.codex/gsd-local-patches" ]; then
|
||||
PATCHES_DIR="$HOME/.codex/gsd-local-patches"
|
||||
else
|
||||
PATCHES_DIR="$HOME/.claude/gsd-local-patches"
|
||||
fi
|
||||
fi
|
||||
# Local install fallback — check all runtime directories
|
||||
if [ ! -d "$PATCHES_DIR" ]; then
|
||||
for dir in .config/opencode .opencode .gemini .claude; do
|
||||
for dir in .config/kilo .kilo .config/opencode .opencode .gemini .codex .claude; do
|
||||
if [ -d "./$dir/gsd-local-patches" ]; then
|
||||
PATCHES_DIR="./$dir/gsd-local-patches"
|
||||
break
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
|
||||
## System Overview
|
||||
|
||||
GSD is a **meta-prompting framework** that sits between the user and AI coding agents (Claude Code, Gemini CLI, OpenCode, Codex, Copilot, Antigravity). It provides:
|
||||
GSD is a **meta-prompting framework** that sits between the user and AI coding agents (Claude Code, Gemini CLI, OpenCode, Kilo, Codex, Copilot, Antigravity). It provides:
|
||||
|
||||
1. **Context engineering** — Structured artifacts that give the AI everything it needs per task
|
||||
2. **Multi-agent orchestration** — Thin orchestrators that spawn specialized agents with fresh context windows
|
||||
@@ -108,7 +108,7 @@ Multiple layers prevent common failure modes:
|
||||
|
||||
User-facing entry points. Each file contains YAML frontmatter (name, description, allowed-tools) and a prompt body that bootstraps the workflow. Commands are installed as:
|
||||
- **Claude Code:** Custom slash commands (`/gsd:command-name`)
|
||||
- **OpenCode:** Slash commands (`/gsd-command-name`)
|
||||
- **OpenCode / Kilo:** Slash commands (`/gsd-command-name`)
|
||||
- **Codex:** Skills (`$gsd-command-name`)
|
||||
- **Copilot:** Slash commands (`/gsd:command-name`)
|
||||
- **Antigravity:** Skills
|
||||
@@ -362,6 +362,7 @@ UI-SPEC.md (per phase) ───────────────────
|
||||
|
||||
Equivalent paths for other runtimes:
|
||||
- **OpenCode:** `~/.config/opencode/` or `~/.opencode/`
|
||||
- **Kilo:** `~/.config/kilo/` or `~/.kilo/`
|
||||
- **Gemini CLI:** `~/.gemini/`
|
||||
- **Codex:** `~/.codex/` (uses skills instead of commands)
|
||||
- **Copilot:** `~/.github/`
|
||||
@@ -425,12 +426,13 @@ Equivalent paths for other runtimes:
|
||||
|
||||
The installer (`bin/install.js`, ~3,000 lines) handles:
|
||||
|
||||
1. **Runtime detection** — Interactive prompt or CLI flags (`--claude`, `--opencode`, `--gemini`, `--codex`, `--copilot`, `--antigravity`, `--all`)
|
||||
1. **Runtime detection** — Interactive prompt or CLI flags (`--claude`, `--opencode`, `--gemini`, `--kilo`, `--codex`, `--copilot`, `--antigravity`, `--all`)
|
||||
2. **Location selection** — Global (`--global`) or local (`--local`)
|
||||
3. **File deployment** — Copies commands, workflows, references, templates, agents, hooks
|
||||
4. **Runtime adaptation** — Transforms file content per runtime:
|
||||
- Claude Code: Uses as-is
|
||||
- OpenCode: Converts agent frontmatter to `name:`, `model: inherit`, `mode: subagent`
|
||||
- OpenCode: Converts commands/agents to OpenCode-compatible flat command + subagent format
|
||||
- Kilo: Reuses the OpenCode conversion pipeline with Kilo config paths
|
||||
- Codex: Generates TOML config + skills from commands
|
||||
- Copilot: Maps tool names (Read→read, Bash→execute, etc.)
|
||||
- Gemini: Adjusts hook event names (`AfterTool` instead of `PostToolUse`)
|
||||
@@ -505,12 +507,13 @@ Debounce: 5 tool uses between repeated warnings. Severity escalation (WARNING→
|
||||
|
||||
## Runtime Abstraction
|
||||
|
||||
GSD supports 6 AI coding runtimes through a unified command/workflow architecture:
|
||||
GSD supports multiple AI coding runtimes through a unified command/workflow architecture:
|
||||
|
||||
| Runtime | Command Format | Agent System | Config Location |
|
||||
|---------|---------------|--------------|-----------------|
|
||||
| Claude Code | `/gsd:command` | Task spawning | `~/.claude/` |
|
||||
| OpenCode | `/gsd-command` | Subagent mode | `~/.config/opencode/` |
|
||||
| Kilo | `/gsd-command` | Subagent mode | `~/.config/kilo/` |
|
||||
| Gemini CLI | `/gsd:command` | Task spawning | `~/.gemini/` |
|
||||
| Codex | `$gsd-command` | Skills | `~/.codex/` |
|
||||
| Copilot | `/gsd:command` | Agent delegation | `~/.github/` |
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
## Command Syntax
|
||||
|
||||
- **Claude Code / Gemini / Copilot:** `/gsd:command-name [args]`
|
||||
- **OpenCode:** `/gsd-command-name [args]`
|
||||
- **OpenCode / Kilo:** `/gsd-command-name [args]`
|
||||
- **Codex:** `$gsd-command-name [args]`
|
||||
|
||||
---
|
||||
|
||||
@@ -342,7 +342,7 @@ Override specific agents without changing the entire profile:
|
||||
|
||||
Valid override values: `opus`, `sonnet`, `haiku`, `inherit`, or any fully-qualified model ID (e.g., `"openai/o3"`, `"google/gemini-2.5-pro"`).
|
||||
|
||||
### Non-Claude Runtimes (Codex, OpenCode, Gemini CLI)
|
||||
### Non-Claude Runtimes (Codex, OpenCode, Gemini CLI, Kilo)
|
||||
|
||||
When GSD is installed for a non-Claude runtime, the installer automatically sets `resolve_model_ids: "omit"` in `~/.gsd/defaults.json`. This causes GSD to return an empty model parameter for all agents, so each agent uses whatever model the runtime is configured with. No additional setup is needed for the default case.
|
||||
|
||||
@@ -377,7 +377,7 @@ The intent is the same as the Claude profile tiers -- use a stronger model for p
|
||||
|-------|----------|----------|
|
||||
| `false` (default) | Returns Claude aliases (`opus`, `sonnet`, `haiku`) | Claude Code with native Anthropic API |
|
||||
| `true` | Maps aliases to full Claude model IDs (`claude-opus-4-0`) | Claude Code with API that requires full IDs |
|
||||
| `"omit"` | Returns empty string (runtime picks its default) | Non-Claude runtimes (Codex, OpenCode, Gemini CLI) |
|
||||
| `"omit"` | Returns empty string (runtime picks its default) | Non-Claude runtimes (Codex, OpenCode, Gemini CLI, Kilo) |
|
||||
|
||||
### Profile Philosophy
|
||||
|
||||
|
||||
@@ -861,10 +861,10 @@ fix(03-01): correct auth token expiry
|
||||
|
||||
### 36. Multi-Runtime Support
|
||||
|
||||
**Purpose:** Run GSD across 6 different AI coding agent runtimes.
|
||||
**Purpose:** Run GSD across multiple AI coding agent runtimes.
|
||||
|
||||
**Requirements:**
|
||||
- REQ-RUNTIME-01: System MUST support Claude Code, OpenCode, Gemini CLI, Codex, Copilot, Antigravity
|
||||
- REQ-RUNTIME-01: System MUST support Claude Code, OpenCode, Gemini CLI, Kilo, Codex, Copilot, Antigravity
|
||||
- REQ-RUNTIME-02: Installer MUST transform content per runtime (tool names, paths, frontmatter)
|
||||
- REQ-RUNTIME-03: Installer MUST support interactive and non-interactive (`--claude --global`) modes
|
||||
- REQ-RUNTIME-04: Installer MUST support both global and local installation
|
||||
@@ -873,12 +873,12 @@ fix(03-01): correct auth token expiry
|
||||
|
||||
**Runtime Transformations:**
|
||||
|
||||
| Aspect | Claude Code | OpenCode | Gemini | Codex | Copilot | Antigravity |
|
||||
|--------|------------|----------|--------|-------|---------|-------------|
|
||||
| Commands | Slash commands | Slash commands | Slash commands | Skills (TOML) | Slash commands | Skills |
|
||||
| Agent format | Claude native | `mode: subagent` | Claude native | Skills | Tool mapping | Skills |
|
||||
| Hook events | `PostToolUse` | N/A | `AfterTool` | N/A | N/A | N/A |
|
||||
| Config | `settings.json` | `opencode.json(c)` | `settings.json` | TOML | Instructions | Config |
|
||||
| Aspect | Claude Code | OpenCode | Gemini | Kilo | Codex | Copilot | Antigravity |
|
||||
|--------|------------|----------|--------|-------|-------|---------|-------------|
|
||||
| Commands | Slash commands | Slash commands | Slash commands | Slash commands | Skills (TOML) | Slash commands | Skills |
|
||||
| Agent format | Claude native | `mode: subagent` | Claude native | `mode: subagent` | Skills | Tool mapping | Skills |
|
||||
| Hook events | `PostToolUse` | N/A | `AfterTool` | N/A | N/A | N/A | N/A |
|
||||
| Config | `settings.json` | `opencode.json(c)` | `settings.json` | `kilo.json(c)` | TOML | Instructions | Config |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -590,7 +590,7 @@ Example quick-task branching:
|
||||
- **quality** -- Opus for all decision-making agents, Sonnet for read-only verification. Use when quota is available and the work is critical.
|
||||
- **balanced** -- Opus only for planning (where architecture decisions happen), Sonnet for everything else. The default for good reason.
|
||||
- **budget** -- Sonnet for anything that writes code, Haiku for research and verification. Use for high-volume work or less critical phases.
|
||||
- **inherit** -- All agents use the current session model. Best when switching models dynamically (e.g. OpenCode `/model`), or when using Claude Code with non-Anthropic providers (OpenRouter, local models) to avoid unexpected API costs. For non-Claude runtimes (Codex, OpenCode, Gemini CLI), the installer sets `resolve_model_ids: "omit"` automatically -- see [Non-Claude Runtimes](#using-non-claude-runtimes-codex-opencode-gemini-cli).
|
||||
- **inherit** -- All agents use the current session model. Best when switching models dynamically (e.g. OpenCode or Kilo `/model`), or when using Claude Code with non-Anthropic providers (OpenRouter, local models) to avoid unexpected API costs. For non-Claude runtimes (Codex, OpenCode, Gemini CLI, Kilo), the installer sets `resolve_model_ids: "omit"` automatically -- see [Non-Claude Runtimes](#using-non-claude-runtimes-codex-opencode-gemini-cli-kilo).
|
||||
|
||||
---
|
||||
|
||||
@@ -733,7 +733,7 @@ Do not re-run `/gsd:execute-phase`. Use `/gsd:quick` for targeted fixes, or `/gs
|
||||
|
||||
Switch to budget profile: `/gsd:set-profile budget`. Disable research and plan-check agents via `/gsd:settings` if the domain is familiar to you (or to Claude).
|
||||
|
||||
### Using Non-Claude Runtimes (Codex, OpenCode, Gemini CLI)
|
||||
### Using Non-Claude Runtimes (Codex, OpenCode, Gemini CLI, Kilo)
|
||||
|
||||
If you installed GSD for a non-Claude runtime, the installer already configured model resolution so all agents use the runtime's default model. No manual setup is needed. Specifically, the installer sets `resolve_model_ids: "omit"` in your config, which tells GSD to skip Anthropic model ID resolution and let the runtime choose its own default model.
|
||||
|
||||
@@ -750,9 +750,9 @@ To assign different models to different agents on a non-Claude runtime, add `mod
|
||||
}
|
||||
```
|
||||
|
||||
The installer auto-configures `resolve_model_ids: "omit"` for Gemini CLI, OpenCode, and Codex. If you're manually setting up a non-Claude runtime, add it to `.planning/config.json` yourself.
|
||||
The installer auto-configures `resolve_model_ids: "omit"` for Gemini CLI, OpenCode, Kilo, and Codex. If you're manually setting up a non-Claude runtime, add it to `.planning/config.json` yourself.
|
||||
|
||||
See the [Configuration Reference](CONFIGURATION.md#non-claude-runtimes-codex-opencode-gemini-cli) for the full explanation.
|
||||
See the [Configuration Reference](CONFIGURATION.md#non-claude-runtimes-codex-opencode-gemini-cli-kilo) for the full explanation.
|
||||
|
||||
### Using Claude Code with Non-Anthropic Providers (OpenRouter, Local)
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
|
||||
## システム概要
|
||||
|
||||
GSDは、ユーザーとAIコーディングエージェント(Claude Code、Gemini CLI、OpenCode、Codex、Copilot、Antigravity)の間に位置する**メタプロンプティングフレームワーク**です。以下の機能を提供します:
|
||||
GSDは、ユーザーとAIコーディングエージェント(Claude Code、Gemini CLI、OpenCode、Kilo、Codex、Copilot、Antigravity)の間に位置する**メタプロンプティングフレームワーク**です。以下の機能を提供します:
|
||||
|
||||
1. **コンテキストエンジニアリング** — タスクごとにAIが必要とするすべてを提供する構造化アーティファクト
|
||||
2. **マルチエージェントオーケストレーション** — 専門エージェントをフレッシュなコンテキストウィンドウで起動する軽量オーケストレーター
|
||||
@@ -108,7 +108,7 @@ GSDは、ユーザーとAIコーディングエージェント(Claude Code、G
|
||||
|
||||
ユーザー向けのエントリーポイントです。各ファイルにはYAMLフロントマター(name、description、allowed-tools)とワークフローをブートストラップするプロンプト本文が含まれています。コマンドは以下の形式でインストールされます:
|
||||
- **Claude Code:** カスタムスラッシュコマンド(`/gsd:command-name`)
|
||||
- **OpenCode:** スラッシュコマンド(`/gsd-command-name`)
|
||||
- **OpenCode / Kilo:** スラッシュコマンド(`/gsd-command-name`)
|
||||
- **Codex:** スキル(`$gsd-command-name`)
|
||||
- **Copilot:** スラッシュコマンド(`/gsd:command-name`)
|
||||
- **Antigravity:** スキル
|
||||
@@ -362,6 +362,7 @@ UI-SPEC.md (per phase) ───────────────────
|
||||
|
||||
他のランタイムでの同等パス:
|
||||
- **OpenCode:** `~/.config/opencode/` または `~/.opencode/`
|
||||
- **Kilo:** `~/.config/kilo/` または `~/.kilo/`
|
||||
- **Gemini CLI:** `~/.gemini/`
|
||||
- **Codex:** `~/.codex/`(コマンドの代わりにスキルを使用)
|
||||
- **Copilot:** `~/.github/`
|
||||
@@ -425,12 +426,13 @@ UI-SPEC.md (per phase) ───────────────────
|
||||
|
||||
インストーラー(`bin/install.js`、約3,000行)は以下を処理します:
|
||||
|
||||
1. **ランタイム検出** — インタラクティブプロンプトまたはCLIフラグ(`--claude`、`--opencode`、`--gemini`、`--codex`、`--copilot`、`--antigravity`、`--all`)
|
||||
1. **ランタイム検出** — インタラクティブプロンプトまたはCLIフラグ(`--claude`、`--opencode`、`--gemini`、`--kilo`、`--codex`、`--copilot`、`--antigravity`、`--all`)
|
||||
2. **インストール先の選択** — グローバル(`--global`)またはローカル(`--local`)
|
||||
3. **ファイルデプロイ** — コマンド、ワークフロー、リファレンス、テンプレート、エージェント、フックをコピー
|
||||
4. **ランタイム適応** — ランタイムごとにファイル内容を変換:
|
||||
- Claude Code: そのまま使用
|
||||
- OpenCode: エージェントフロントマターを `name:`、`model: inherit`、`mode: subagent` に変換
|
||||
- OpenCode: コマンド/エージェントをOpenCode互換のフラットコマンド + サブエージェント形式に変換
|
||||
- Kilo: OpenCode変換パイプラインをKiloの設定パスで再利用
|
||||
- Codex: コマンドからTOML設定 + スキルを生成
|
||||
- Copilot: ツール名をマッピング(Read→read、Bash→executeなど)
|
||||
- Gemini: フックイベント名を調整(`PostToolUse` の代わりに `AfterTool`)
|
||||
@@ -505,12 +507,13 @@ Runtime Engine (Claude Code / Gemini CLI)
|
||||
|
||||
## ランタイム抽象化
|
||||
|
||||
GSDは統一されたコマンド/ワークフローアーキテクチャを通じて6つのAIコーディングランタイムをサポートしています:
|
||||
GSDは統一されたコマンド/ワークフローアーキテクチャを通じて複数のAIコーディングランタイムをサポートしています:
|
||||
|
||||
| ランタイム | コマンド形式 | エージェントシステム | 設定場所 |
|
||||
|---------|---------------|--------------|-----------------|
|
||||
| Claude Code | `/gsd:command` | Task起動 | `~/.claude/` |
|
||||
| OpenCode | `/gsd-command` | サブエージェントモード | `~/.config/opencode/` |
|
||||
| Kilo | `/gsd-command` | サブエージェントモード | `~/.config/kilo/` |
|
||||
| Gemini CLI | `/gsd:command` | Task起動 | `~/.gemini/` |
|
||||
| Codex | `$gsd-command` | スキル | `~/.codex/` |
|
||||
| Copilot | `/gsd:command` | エージェント委譲 | `~/.github/` |
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
## コマンド構文
|
||||
|
||||
- **Claude Code / Gemini / Copilot:** `/gsd:command-name [args]`
|
||||
- **OpenCode:** `/gsd-command-name [args]`
|
||||
- **OpenCode / Kilo:** `/gsd-command-name [args]`
|
||||
- **Codex:** `$gsd-command-name [args]`
|
||||
|
||||
---
|
||||
|
||||
@@ -275,7 +275,7 @@ quick タスクのブランチ設定例:
|
||||
|
||||
有効なオーバーライド値: `opus`、`sonnet`、`haiku`、`inherit`、または完全修飾モデル ID(例: `"openai/o3"`、`"google/gemini-2.5-pro"`)。
|
||||
|
||||
### 非 Claude ランタイム(Codex、OpenCode、Gemini CLI)
|
||||
### 非 Claude ランタイム(Codex、OpenCode、Gemini CLI、Kilo)
|
||||
|
||||
GSD が非 Claude ランタイム向けにインストールされると、インストーラーは自動的に `~/.gsd/defaults.json` に `resolve_model_ids: "omit"` を設定します。これにより GSD はすべてのエージェントに対して空のモデルパラメータを返し、各エージェントはランタイムで設定されたモデルを使用します。デフォルトの場合、追加のセットアップは不要です。
|
||||
|
||||
@@ -310,7 +310,7 @@ GSD が非 Claude ランタイム向けにインストールされると、イ
|
||||
|----|------|---------|
|
||||
| `false`(デフォルト) | Claude エイリアス(`opus`、`sonnet`、`haiku`)を返す | Claude Code + ネイティブ Anthropic API |
|
||||
| `true` | エイリアスを完全な Claude モデル ID(`claude-opus-4-0`)にマッピング | 完全な ID が必要な API を使用する Claude Code |
|
||||
| `"omit"` | 空文字列を返す(ランタイムがデフォルトを選択) | 非 Claude ランタイム(Codex、OpenCode、Gemini CLI) |
|
||||
| `"omit"` | 空文字列を返す(ランタイムがデフォルトを選択) | 非 Claude ランタイム(Codex、OpenCode、Gemini CLI、Kilo) |
|
||||
|
||||
### プロファイルの設計思想
|
||||
|
||||
|
||||
@@ -861,10 +861,10 @@ fix(03-01): correct auth token expiry
|
||||
|
||||
### 36. マルチランタイムサポート
|
||||
|
||||
**目的:** 6つの異なる AI コーディングエージェントランタイムで GSD を実行します。
|
||||
**目的:** 複数の AI コーディングエージェントランタイムで GSD を実行します。
|
||||
|
||||
**要件:**
|
||||
- REQ-RUNTIME-01: システムは Claude Code、OpenCode、Gemini CLI、Codex、Copilot、Antigravity をサポートしなければならない
|
||||
- REQ-RUNTIME-01: システムは Claude Code、OpenCode、Gemini CLI、Kilo、Codex、Copilot、Antigravity をサポートしなければならない
|
||||
- REQ-RUNTIME-02: インストーラーはランタイムごとにコンテンツを変換しなければならない(ツール名、パス、フロントマター)
|
||||
- REQ-RUNTIME-03: インストーラーはインタラクティブおよび非インタラクティブ(`--claude --global`)モードをサポートしなければならない
|
||||
- REQ-RUNTIME-04: インストーラーはグローバルとローカルの両方のインストールをサポートしなければならない
|
||||
@@ -873,12 +873,12 @@ fix(03-01): correct auth token expiry
|
||||
|
||||
**ランタイム変換:**
|
||||
|
||||
| 側面 | Claude Code | OpenCode | Gemini | Codex | Copilot | Antigravity |
|
||||
|------|------------|----------|--------|-------|---------|-------------|
|
||||
| コマンド | スラッシュコマンド | スラッシュコマンド | スラッシュコマンド | スキル(TOML) | スラッシュコマンド | スキル |
|
||||
| エージェント形式 | Claude ネイティブ | `mode: subagent` | Claude ネイティブ | スキル | ツールマッピング | スキル |
|
||||
| フックイベント | `PostToolUse` | N/A | `AfterTool` | N/A | N/A | N/A |
|
||||
| 設定 | `settings.json` | `opencode.json(c)` | `settings.json` | TOML | Instructions | Config |
|
||||
| 側面 | Claude Code | OpenCode | Gemini | Kilo | Codex | Copilot | Antigravity |
|
||||
|------|------------|----------|--------|-------|-------|---------|-------------|
|
||||
| コマンド | スラッシュコマンド | スラッシュコマンド | スラッシュコマンド | スラッシュコマンド | スキル(TOML) | スラッシュコマンド | スキル |
|
||||
| エージェント形式 | Claude ネイティブ | `mode: subagent` | Claude ネイティブ | `mode: subagent` | スキル | ツールマッピング | スキル |
|
||||
| フックイベント | `PostToolUse` | N/A | `AfterTool` | N/A | N/A | N/A | N/A |
|
||||
| 設定 | `settings.json` | `opencode.json(c)` | `settings.json` | `kilo.json(c)` | TOML | Instructions | Config |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -580,7 +580,7 @@ quick タスクのブランチ設定例:
|
||||
- **quality** -- すべての意思決定エージェントに Opus、読み取り専用の検証に Sonnet。クォータに余裕があり、重要な作業に使用。
|
||||
- **balanced** -- プランニング(アーキテクチャの決定が行われる場所)にのみ Opus、それ以外は Sonnet。正当な理由があるデフォルト。
|
||||
- **budget** -- コードを書くものには Sonnet、リサーチと検証には Haiku。大量作業や重要度の低いフェーズに使用。
|
||||
- **inherit** -- すべてのエージェントが現在のセッションモデルを使用。モデルを動的に切り替える場合(例:OpenCode の `/model`)や、Claude Code を非 Anthropic プロバイダー(OpenRouter、ローカルモデル)で使用する場合に最適で、予期しない API コストを回避できます。非 Claude ランタイム(Codex、OpenCode、Gemini CLI)では、インストーラーが自動的に `resolve_model_ids: "omit"` を設定します -- [非 Claude ランタイムの使用](#非-claude-ランタイムの使用codexopencodegemini-cli)を参照。
|
||||
- **inherit** -- すべてのエージェントが現在のセッションモデルを使用。モデルを動的に切り替える場合(例:OpenCode または Kilo の `/model`)や、Claude Code を非 Anthropic プロバイダー(OpenRouter、ローカルモデル)で使用する場合に最適で、予期しない API コストを回避できます。非 Claude ランタイム(Codex、OpenCode、Gemini CLI、Kilo)では、インストーラーが自動的に `resolve_model_ids: "omit"` を設定します -- [非 Claude ランタイムの使用](#非-claude-ランタイムの使用codexopencodegemini-clikilo)を参照。
|
||||
|
||||
---
|
||||
|
||||
@@ -723,7 +723,7 @@ cd ~/gsd-workspaces/feature-b
|
||||
|
||||
budget プロファイルに切り替えてください:`/gsd:set-profile budget`。ドメインに慣れている場合(またはClaude が慣れている場合)は、`/gsd:settings` でリサーチエージェントと plan-check エージェントを無効にしてください。
|
||||
|
||||
### 非 Claude ランタイムの使用(Codex、OpenCode、Gemini CLI)
|
||||
### 非 Claude ランタイムの使用(Codex、OpenCode、Gemini CLI、Kilo)
|
||||
|
||||
非 Claude ランタイム用に GSD をインストールした場合、インストーラーがモデル解決を設定済みのため、すべてのエージェントがランタイムのデフォルトモデルを使用します。手動設定は不要です。具体的には、インストーラーが設定に `resolve_model_ids: "omit"` を設定し、GSD に Anthropic モデル ID の解決をスキップしてランタイム独自のデフォルトモデルを使用するよう指示します。
|
||||
|
||||
@@ -740,9 +740,9 @@ budget プロファイルに切り替えてください:`/gsd:set-profile budg
|
||||
}
|
||||
```
|
||||
|
||||
インストーラーは Gemini CLI、OpenCode、Codex 用に `resolve_model_ids: "omit"` を自動設定します。非 Claude ランタイムを手動で設定する場合は、`.planning/config.json` に自分で追加してください。
|
||||
インストーラーは Gemini CLI、OpenCode、Kilo、Codex 用に `resolve_model_ids: "omit"` を自動設定します。非 Claude ランタイムを手動で設定する場合は、`.planning/config.json` に自分で追加してください。
|
||||
|
||||
完全な説明は[設定リファレンス](../CONFIGURATION.md#non-claude-runtimes-codex-opencode-gemini-cli)をご覧ください。
|
||||
完全な説明は[設定リファレンス](../CONFIGURATION.md#non-claude-runtimes-codex-opencode-gemini-cli-kilo)をご覧ください。
|
||||
|
||||
### 非 Anthropic プロバイダーでの Claude Code の使用(OpenRouter、ローカル)
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
|
||||
## 시스템 개요
|
||||
|
||||
GSD는 사용자와 AI 코딩 에이전트(Claude Code, Gemini CLI, OpenCode, Codex, Copilot, Antigravity) 사이에 위치하는 **메타 프롬프팅 프레임워크**입니다. 다음을 제공합니다.
|
||||
GSD는 사용자와 AI 코딩 에이전트(Claude Code, Gemini CLI, OpenCode, Kilo, Codex, Copilot, Antigravity) 사이에 위치하는 **메타 프롬프팅 프레임워크**입니다. 다음을 제공합니다.
|
||||
|
||||
1. **컨텍스트 엔지니어링** — 작업별로 AI에게 필요한 모든 것을 제공하는 구조화된 아티팩트
|
||||
2. **멀티 에이전트 오케스트레이션** — 새로운 컨텍스트 윈도우로 전문화된 에이전트를 생성하는 가벼운 오케스트레이터
|
||||
@@ -108,7 +108,7 @@ GSD는 사용자와 AI 코딩 에이전트(Claude Code, Gemini CLI, OpenCode, Co
|
||||
|
||||
사용자 대면 진입점입니다. 각 파일은 YAML 전문(name, description, allowed-tools)과 워크플로우를 부트스트랩하는 프롬프트 본문을 포함합니다. 명령어는 다음과 같이 설치됩니다.
|
||||
- **Claude Code:** 커스텀 슬래시 명령어 (`/gsd:command-name`)
|
||||
- **OpenCode:** 슬래시 명령어 (`/gsd-command-name`)
|
||||
- **OpenCode / Kilo:** 슬래시 명령어 (`/gsd-command-name`)
|
||||
- **Codex:** Skills (`$gsd-command-name`)
|
||||
- **Copilot:** 슬래시 명령어 (`/gsd:command-name`)
|
||||
- **Antigravity:** Skills
|
||||
@@ -362,6 +362,7 @@ UI-SPEC.md (per phase) ───────────────────
|
||||
|
||||
다른 런타임의 동등한 경로입니다.
|
||||
- **OpenCode:** `~/.config/opencode/` 또는 `~/.opencode/`
|
||||
- **Kilo:** `~/.config/kilo/` 또는 `~/.kilo/`
|
||||
- **Gemini CLI:** `~/.gemini/`
|
||||
- **Codex:** `~/.codex/` (명령어 대신 skills 사용)
|
||||
- **Copilot:** `~/.github/`
|
||||
@@ -425,12 +426,13 @@ UI-SPEC.md (per phase) ───────────────────
|
||||
|
||||
인스톨러(`bin/install.js`, ~3,000줄)는 다음을 처리합니다.
|
||||
|
||||
1. **런타임 감지** — 대화형 프롬프트 또는 CLI 플래그 (`--claude`, `--opencode`, `--gemini`, `--codex`, `--copilot`, `--antigravity`, `--all`)
|
||||
1. **런타임 감지** — 대화형 프롬프트 또는 CLI 플래그 (`--claude`, `--opencode`, `--gemini`, `--kilo`, `--codex`, `--copilot`, `--antigravity`, `--all`)
|
||||
2. **위치 선택** — 전역(`--global`) 또는 로컬(`--local`)
|
||||
3. **파일 배포** — commands, workflows, references, templates, agents, hooks 복사
|
||||
4. **런타임 적응** — 런타임별 파일 내용 변환.
|
||||
- Claude Code: 그대로 사용
|
||||
- OpenCode: 에이전트 전문을 `name:`, `model: inherit`, `mode: subagent`로 변환
|
||||
- OpenCode: 명령어/에이전트를 OpenCode 호환 플랫 명령어 + 서브에이전트 형식으로 변환
|
||||
- Kilo: Kilo 설정 경로로 OpenCode 변환 파이프라인을 재사용
|
||||
- Codex: commands에서 TOML config + skills 생성
|
||||
- Copilot: 도구 이름 매핑 (Read→read, Bash→execute 등)
|
||||
- Gemini: 훅 이벤트 이름 조정 (`PostToolUse` 대신 `AfterTool`)
|
||||
@@ -505,12 +507,13 @@ Runtime Engine (Claude Code / Gemini CLI)
|
||||
|
||||
## 런타임 추상화
|
||||
|
||||
GSD는 통합된 명령어/워크플로우 아키텍처를 통해 6개의 AI 코딩 런타임을 지원합니다.
|
||||
GSD는 통합된 명령어/워크플로우 아키텍처를 통해 여러 AI 코딩 런타임을 지원합니다.
|
||||
|
||||
| 런타임 | 명령어 형식 | 에이전트 시스템 | 설정 위치 |
|
||||
|---------|---------------|--------------|-----------------|
|
||||
| Claude Code | `/gsd:command` | Task 생성 | `~/.claude/` |
|
||||
| OpenCode | `/gsd-command` | Subagent 모드 | `~/.config/opencode/` |
|
||||
| Kilo | `/gsd-command` | Subagent 모드 | `~/.config/kilo/` |
|
||||
| Gemini CLI | `/gsd:command` | Task 생성 | `~/.gemini/` |
|
||||
| Codex | `$gsd-command` | Skills | `~/.codex/` |
|
||||
| Copilot | `/gsd:command` | 에이전트 위임 | `~/.github/` |
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
## 명령어 문법
|
||||
|
||||
- **Claude Code / Gemini / Copilot:** `/gsd:command-name [args]`
|
||||
- **OpenCode:** `/gsd-command-name [args]`
|
||||
- **OpenCode / Kilo:** `/gsd-command-name [args]`
|
||||
- **Codex:** `$gsd-command-name [args]`
|
||||
|
||||
---
|
||||
|
||||
@@ -275,7 +275,7 @@ quick 태스크 브랜칭 예시:
|
||||
|
||||
유효한 재정의 값: `opus`, `sonnet`, `haiku`, `inherit`, 또는 완전히 정규화된 모델 ID (예: `"openai/o3"`, `"google/gemini-2.5-pro"`).
|
||||
|
||||
### 비 Claude 런타임 (Codex, OpenCode, Gemini CLI)
|
||||
### 비 Claude 런타임 (Codex, OpenCode, Gemini CLI, Kilo)
|
||||
|
||||
비 Claude 런타임에 GSD를 설치하면 인스톨러가 자동으로 `~/.gsd/defaults.json`에 `resolve_model_ids: "omit"`을 설정합니다. 이로 인해 GSD는 모든 에이전트에 빈 model 파라미터를 반환하며 각 에이전트는 런타임에 설정된 모델을 사용합니다. 기본 사용 시 추가 설정은 필요하지 않습니다.
|
||||
|
||||
@@ -310,7 +310,7 @@ quick 태스크 브랜칭 예시:
|
||||
|----|------|----------|
|
||||
| `false` (기본값) | Claude 별칭 반환 (`opus`, `sonnet`, `haiku`) | 네이티브 Anthropic API를 사용하는 Claude Code |
|
||||
| `true` | 별칭을 전체 Claude 모델 ID로 매핑 (`claude-opus-4-0`) | 전체 ID가 필요한 API를 사용하는 Claude Code |
|
||||
| `"omit"` | 빈 문자열 반환 (런타임이 기본값 선택) | 비 Claude 런타임 (Codex, OpenCode, Gemini CLI) |
|
||||
| `"omit"` | 빈 문자열 반환 (런타임이 기본값 선택) | 비 Claude 런타임 (Codex, OpenCode, Gemini CLI, Kilo) |
|
||||
|
||||
### 프로필 철학
|
||||
|
||||
|
||||
@@ -861,10 +861,10 @@ fix(03-01): correct auth token expiry
|
||||
|
||||
### 36. Multi-Runtime Support
|
||||
|
||||
**목적:** 6가지 다른 AI 코딩 에이전트 런타임에서 GSD를 실행합니다.
|
||||
**목적:** 여러 AI 코딩 에이전트 런타임에서 GSD를 실행합니다.
|
||||
|
||||
**요구사항.**
|
||||
- REQ-RUNTIME-01: Claude Code, OpenCode, Gemini CLI, Codex, Copilot, Antigravity를 지원해야 합니다.
|
||||
- REQ-RUNTIME-01: Claude Code, OpenCode, Gemini CLI, Kilo, Codex, Copilot, Antigravity를 지원해야 합니다.
|
||||
- REQ-RUNTIME-02: 설치 프로그램은 런타임별로 콘텐츠를 변환해야 합니다(도구 이름, 경로, 프론트매터).
|
||||
- REQ-RUNTIME-03: 설치 프로그램은 대화형 및 비대화형(`--claude --global`) 모드를 모두 지원해야 합니다.
|
||||
- REQ-RUNTIME-04: 설치 프로그램은 전역 및 로컬 설치를 모두 지원해야 합니다.
|
||||
@@ -873,12 +873,12 @@ fix(03-01): correct auth token expiry
|
||||
|
||||
**런타임 변환.**
|
||||
|
||||
| 측면 | Claude Code | OpenCode | Gemini | Codex | Copilot | Antigravity |
|
||||
|--------|------------|----------|--------|-------|---------|-------------|
|
||||
| 명령어 | 슬래시 명령어 | 슬래시 명령어 | 슬래시 명령어 | Skills(TOML) | 슬래시 명령어 | Skills |
|
||||
| 에이전트 형식 | Claude native | `mode: subagent` | Claude native | Skills | Tool mapping | Skills |
|
||||
| 훅 이벤트 | `PostToolUse` | N/A | `AfterTool` | N/A | N/A | N/A |
|
||||
| 구성 | `settings.json` | `opencode.json(c)` | `settings.json` | TOML | Instructions | Config |
|
||||
| 측면 | Claude Code | OpenCode | Gemini | Kilo | Codex | Copilot | Antigravity |
|
||||
|--------|------------|----------|--------|-------|-------|---------|-------------|
|
||||
| 명령어 | 슬래시 명령어 | 슬래시 명령어 | 슬래시 명령어 | 슬래시 명령어 | Skills(TOML) | 슬래시 명령어 | Skills |
|
||||
| 에이전트 형식 | Claude native | `mode: subagent` | Claude native | `mode: subagent` | Skills | Tool mapping | Skills |
|
||||
| 훅 이벤트 | `PostToolUse` | N/A | `AfterTool` | N/A | N/A | N/A | N/A |
|
||||
| 구성 | `settings.json` | `opencode.json(c)` | `settings.json` | `kilo.json(c)` | TOML | Instructions | Config |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -580,7 +580,7 @@ GSD는 프로젝트 설정을 `.planning/config.json`에 저장합니다. `/gsd:
|
||||
- **quality** -- 모든 의사결정 에이전트에 Opus를 사용하고 읽기 전용 검증에 Sonnet을 사용합니다. 할당량이 충분하고 작업이 중요할 때 사용합니다.
|
||||
- **balanced** -- 아키텍처 결정이 이루어지는 계획에만 Opus를 사용하고 나머지는 Sonnet을 사용합니다. 합당한 이유로 기본값입니다.
|
||||
- **budget** -- 코드를 작성하는 모든 것에 Sonnet을 사용하고 조사 및 검증에 Haiku를 사용합니다. 대량 작업이나 덜 중요한 페이즈에 사용합니다.
|
||||
- **inherit** -- 모든 에이전트가 현재 세션 모델을 사용합니다. 동적으로 모델을 전환할 때 (예: OpenCode `/model`) 또는 예상치 못한 API 비용을 방지하기 위해 비Anthropic 공급자 (OpenRouter, 로컬 모델)와 함께 Claude Code를 사용할 때 적합합니다. 비Claude 런타임 (Codex, OpenCode, Gemini CLI)의 경우 설치 프로그램이 자동으로 `resolve_model_ids: "omit"`을 설정합니다 — [비Claude 런타임](#비claude-런타임-codex-opencode-gemini-cli-사용)을 참고하세요.
|
||||
- **inherit** -- 모든 에이전트가 현재 세션 모델을 사용합니다. 동적으로 모델을 전환할 때 (예: OpenCode 또는 Kilo `/model`) 또는 예상치 못한 API 비용을 방지하기 위해 비Anthropic 공급자 (OpenRouter, 로컬 모델)와 함께 Claude Code를 사용할 때 적합합니다. 비Claude 런타임 (Codex, OpenCode, Gemini CLI, Kilo)의 경우 설치 프로그램이 자동으로 `resolve_model_ids: "omit"`을 설정합니다 — [비Claude 런타임](#비claude-런타임-codex-opencode-gemini-cli-kilo-사용)을 참고하세요.
|
||||
|
||||
---
|
||||
|
||||
@@ -723,7 +723,7 @@ cd ~/gsd-workspaces/feature-b
|
||||
|
||||
예산 프로필로 전환하세요: `/gsd:set-profile budget`. 도메인이 익숙하다면 (또는 Claude에게 익숙하다면) `/gsd:settings`에서 조사 및 plan-check 에이전트를 비활성화하세요.
|
||||
|
||||
### 비Claude 런타임 사용 (Codex, OpenCode, Gemini CLI)
|
||||
### 비Claude 런타임 사용 (Codex, OpenCode, Gemini CLI, Kilo)
|
||||
|
||||
비Claude 런타임용으로 GSD를 설치했다면 설치 프로그램이 이미 모든 에이전트가 런타임의 기본 모델을 사용하도록 모델 해석을 구성했습니다. 수동 설정이 필요하지 않습니다. 구체적으로 설치 프로그램은 config에 `resolve_model_ids: "omit"`을 설정하여 GSD가 Anthropic 모델 ID 해석을 건너뛰고 런타임이 자체 기본 모델을 선택하도록 합니다.
|
||||
|
||||
@@ -740,9 +740,9 @@ cd ~/gsd-workspaces/feature-b
|
||||
}
|
||||
```
|
||||
|
||||
설치 프로그램은 Gemini CLI, OpenCode, Codex에 대해 `resolve_model_ids: "omit"`을 자동으로 구성합니다. 비Claude 런타임을 수동으로 설정하는 경우 직접 `.planning/config.json`에 추가하세요.
|
||||
설치 프로그램은 Gemini CLI, OpenCode, Kilo, Codex에 대해 `resolve_model_ids: "omit"`을 자동으로 구성합니다. 비Claude 런타임을 수동으로 설정하는 경우 직접 `.planning/config.json`에 추가하세요.
|
||||
|
||||
전체 설명은 [Configuration Reference](CONFIGURATION.md#non-claude-runtimes-codex-opencode-gemini-cli)를 참고하세요.
|
||||
전체 설명은 [Configuration Reference](CONFIGURATION.md#non-claude-runtimes-codex-opencode-gemini-cli-kilo)를 참고하세요.
|
||||
|
||||
### 비Anthropic 공급자와 함께 Claude Code 사용 (OpenRouter, 로컬)
|
||||
|
||||
|
||||
@@ -283,7 +283,7 @@ Use perfil budget:
|
||||
/gsd:set-profile budget
|
||||
```
|
||||
|
||||
### Runtime não-Claude (Codex/OpenCode/Gemini)
|
||||
### Runtime não-Claude (Codex/OpenCode/Gemini/Kilo)
|
||||
|
||||
Use `resolve_model_ids: "omit"` para deixar o runtime resolver modelos padrão.
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
# GET SHIT DONE
|
||||
|
||||
**一个轻量级且强大的元提示、上下文工程和规格驱动开发系统,支持 Claude Code、OpenCode、Gemini CLI 和 Codex。**
|
||||
**一个轻量级且强大的元提示、上下文工程和规格驱动开发系统,支持 Claude Code、OpenCode、Gemini CLI、Kilo 和 Codex。**
|
||||
|
||||
**解决上下文衰减 —— 即 Claude 填充上下文窗口时发生的质量退化问题。**
|
||||
|
||||
@@ -80,12 +80,13 @@ npx get-shit-done-cc@latest
|
||||
```
|
||||
|
||||
安装程序会提示你选择:
|
||||
1. **运行时** —— Claude Code、OpenCode、Gemini、Codex 或全部
|
||||
1. **运行时** —— Claude Code、OpenCode、Gemini、Kilo、Codex 或全部
|
||||
2. **位置** —— 全局(所有项目)或本地(仅当前项目)
|
||||
|
||||
验证安装:
|
||||
- Claude Code / Gemini: `/gsd:help`
|
||||
- OpenCode: `/gsd-help`
|
||||
- Kilo: `/gsd-help`
|
||||
- Codex: `$gsd-help`
|
||||
|
||||
> [!NOTE]
|
||||
@@ -113,6 +114,10 @@ npx get-shit-done-cc --opencode --global # 安装到 ~/.config/opencode/
|
||||
# Gemini CLI
|
||||
npx get-shit-done-cc --gemini --global # 安装到 ~/.gemini/
|
||||
|
||||
# Kilo(OpenCode 分支)
|
||||
npx get-shit-done-cc --kilo --global # 安装到 ~/.config/kilo/
|
||||
npx get-shit-done-cc --kilo --local # 安装到 ./.kilo/
|
||||
|
||||
# Codex(技能优先)
|
||||
npx get-shit-done-cc --codex --global # 安装到 ~/.codex/
|
||||
npx get-shit-done-cc --codex --local # 安装到 ./.codex/
|
||||
@@ -122,7 +127,7 @@ npx get-shit-done-cc --all --global # 安装到所有目录
|
||||
```
|
||||
|
||||
使用 `--global`(`-g`)或 `--local`(`-l`)跳过位置提示。
|
||||
使用 `--claude`、`--opencode`、`--gemini`、`--codex` 或 `--all` 跳过运行时提示。
|
||||
使用 `--claude`、`--opencode`、`--gemini`、`--kilo`、`--codex` 或 `--all` 跳过运行时提示。
|
||||
|
||||
</details>
|
||||
|
||||
@@ -657,11 +662,13 @@ CLAUDE_CONFIG_DIR=/home/youruser/.claude npx get-shit-done-cc --global
|
||||
# 全局安装
|
||||
npx get-shit-done-cc --claude --global --uninstall
|
||||
npx get-shit-done-cc --opencode --global --uninstall
|
||||
npx get-shit-done-cc --kilo --global --uninstall
|
||||
npx get-shit-done-cc --codex --global --uninstall
|
||||
|
||||
# 本地安装(当前项目)
|
||||
npx get-shit-done-cc --claude --local --uninstall
|
||||
npx get-shit-done-cc --opencode --local --uninstall
|
||||
npx get-shit-done-cc --kilo --local --uninstall
|
||||
npx get-shit-done-cc --codex --local --uninstall
|
||||
```
|
||||
|
||||
@@ -671,7 +678,7 @@ npx get-shit-done-cc --codex --local --uninstall
|
||||
|
||||
## 社区移植
|
||||
|
||||
OpenCode、Gemini CLI 和 Codex 现在通过 `npx get-shit-done-cc` 原生支持。
|
||||
OpenCode、Gemini CLI、Kilo 和 Codex 现在通过 `npx get-shit-done-cc` 原生支持。
|
||||
|
||||
这些社区移植开创了多运行时支持:
|
||||
|
||||
|
||||
@@ -39,11 +39,11 @@ Model profiles control which Claude model each GSD agent uses. This allows balan
|
||||
|
||||
**inherit** - Follow the current session model
|
||||
- All agents resolve to `inherit`
|
||||
- Best when you switch models interactively (for example OpenCode `/model`)
|
||||
- Best when you switch models interactively (for example OpenCode or Kilo `/model`)
|
||||
- **Required when using non-Anthropic providers** (OpenRouter, local models, etc.) — otherwise GSD may call Anthropic models directly, incurring unexpected costs
|
||||
- Use when: you want GSD to follow your currently selected runtime model
|
||||
|
||||
## Using Non-Claude Runtimes (Codex, OpenCode, Gemini CLI)
|
||||
## Using Non-Claude Runtimes (Codex, OpenCode, Gemini CLI, Kilo)
|
||||
|
||||
When installed for a non-Claude runtime, the GSD installer sets `resolve_model_ids: "omit"` in `~/.gsd/defaults.json`. This returns an empty model parameter for all agents, so each agent uses the runtime's default model. No manual setup is needed.
|
||||
|
||||
|
||||
@@ -11,28 +11,55 @@ Read all files referenced by the invoking prompt's execution_context before star
|
||||
<step name="get_installed_version">
|
||||
Detect whether GSD is installed locally or globally by checking both locations and validating install integrity.
|
||||
|
||||
First, derive `PREFERRED_RUNTIME` from the invoking prompt's `execution_context` path:
|
||||
First, derive `PREFERRED_CONFIG_DIR` and `PREFERRED_RUNTIME` from the invoking prompt's `execution_context` path:
|
||||
- If the path contains `/get-shit-done/workflows/update.md`, strip that suffix and store the remainder as `PREFERRED_CONFIG_DIR`
|
||||
- Path contains `/.codex/` -> `codex`
|
||||
- Path contains `/.gemini/` -> `gemini`
|
||||
- Path contains `/.config/opencode/` or `/.opencode/` -> `opencode`
|
||||
- Path contains `/.config/kilo/` or `/.kilo/`, or `PREFERRED_CONFIG_DIR` contains `kilo.json` / `kilo.jsonc` -> `kilo`
|
||||
- Path contains `/.config/opencode/` or `/.opencode/`, or `PREFERRED_CONFIG_DIR` contains `opencode.json` / `opencode.jsonc` -> `opencode`
|
||||
- Otherwise -> `claude`
|
||||
|
||||
Use `PREFERRED_CONFIG_DIR` when available so custom `--config-dir` installs are checked before default locations.
|
||||
Use `PREFERRED_RUNTIME` as the first runtime checked so `/gsd:update` targets the runtime that invoked it.
|
||||
|
||||
```bash
|
||||
expand_home() {
|
||||
case "$1" in
|
||||
"~/"*) printf '%s/%s\n' "$HOME" "${1#~/}" ;;
|
||||
*) printf '%s\n' "$1" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Runtime candidates: "<runtime>:<config-dir>" stored as an array.
|
||||
# Using an array instead of a space-separated string ensures correct
|
||||
# iteration in both bash and zsh (zsh does not word-split unquoted
|
||||
# variables by default). Fixes #1173.
|
||||
RUNTIME_DIRS=( "claude:.claude" "opencode:.config/opencode" "opencode:.opencode" "gemini:.gemini" "codex:.codex" )
|
||||
RUNTIME_DIRS=( "claude:.claude" "opencode:.config/opencode" "opencode:.opencode" "gemini:.gemini" "kilo:.config/kilo" "kilo:.kilo" "codex:.codex" )
|
||||
ENV_RUNTIME_DIRS=()
|
||||
|
||||
# PREFERRED_RUNTIME should be set from execution_context before running this block.
|
||||
# If not set, infer from runtime env vars; fallback to claude.
|
||||
# PREFERRED_CONFIG_DIR / PREFERRED_RUNTIME should be set from execution_context
|
||||
# before running this block.
|
||||
if [ -n "$PREFERRED_CONFIG_DIR" ]; then
|
||||
PREFERRED_CONFIG_DIR="$(expand_home "$PREFERRED_CONFIG_DIR")"
|
||||
if [ -z "$PREFERRED_RUNTIME" ]; then
|
||||
if [ -f "$PREFERRED_CONFIG_DIR/kilo.json" ] || [ -f "$PREFERRED_CONFIG_DIR/kilo.jsonc" ]; then
|
||||
PREFERRED_RUNTIME="kilo"
|
||||
elif [ -f "$PREFERRED_CONFIG_DIR/opencode.json" ] || [ -f "$PREFERRED_CONFIG_DIR/opencode.jsonc" ]; then
|
||||
PREFERRED_RUNTIME="opencode"
|
||||
elif [ -f "$PREFERRED_CONFIG_DIR/config.toml" ]; then
|
||||
PREFERRED_RUNTIME="codex"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# If runtime is still unknown, infer from runtime env vars; fallback to claude.
|
||||
if [ -z "$PREFERRED_RUNTIME" ]; then
|
||||
if [ -n "$CODEX_HOME" ]; then
|
||||
PREFERRED_RUNTIME="codex"
|
||||
elif [ -n "$GEMINI_CONFIG_DIR" ]; then
|
||||
PREFERRED_RUNTIME="gemini"
|
||||
elif [ -n "$KILO_CONFIG_DIR" ]; then
|
||||
PREFERRED_RUNTIME="kilo"
|
||||
elif [ -n "$OPENCODE_CONFIG_DIR" ] || [ -n "$OPENCODE_CONFIG" ]; then
|
||||
PREFERRED_RUNTIME="opencode"
|
||||
elif [ -n "$CLAUDE_CONFIG_DIR" ]; then
|
||||
@@ -42,6 +69,54 @@ if [ -z "$PREFERRED_RUNTIME" ]; then
|
||||
fi
|
||||
fi
|
||||
|
||||
# If execution_context already points at an installed config dir, trust it first.
|
||||
# This covers custom --config-dir installs that do not live under the default
|
||||
# runtime directories.
|
||||
if [ -n "$PREFERRED_CONFIG_DIR" ] && { [ -f "$PREFERRED_CONFIG_DIR/get-shit-done/VERSION" ] || [ -f "$PREFERRED_CONFIG_DIR/get-shit-done/workflows/update.md" ]; }; then
|
||||
INSTALL_SCOPE="GLOBAL"
|
||||
for dir in .claude .config/opencode .opencode .gemini .config/kilo .kilo .codex; do
|
||||
resolved_local="$(cd "./$dir" 2>/dev/null && pwd)"
|
||||
if [ -n "$resolved_local" ] && [ "$resolved_local" = "$PREFERRED_CONFIG_DIR" ]; then
|
||||
INSTALL_SCOPE="LOCAL"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -f "$PREFERRED_CONFIG_DIR/get-shit-done/VERSION" ] && grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+' "$PREFERRED_CONFIG_DIR/get-shit-done/VERSION"; then
|
||||
INSTALLED_VERSION="$(cat "$PREFERRED_CONFIG_DIR/get-shit-done/VERSION")"
|
||||
else
|
||||
INSTALLED_VERSION="0.0.0"
|
||||
fi
|
||||
|
||||
echo "$INSTALLED_VERSION"
|
||||
echo "$INSTALL_SCOPE"
|
||||
echo "${PREFERRED_RUNTIME:-claude}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Absolute global candidates from env overrides (covers custom config dirs).
|
||||
if [ -n "$CLAUDE_CONFIG_DIR" ]; then
|
||||
ENV_RUNTIME_DIRS+=( "claude:$(expand_home "$CLAUDE_CONFIG_DIR")" )
|
||||
fi
|
||||
if [ -n "$GEMINI_CONFIG_DIR" ]; then
|
||||
ENV_RUNTIME_DIRS+=( "gemini:$(expand_home "$GEMINI_CONFIG_DIR")" )
|
||||
fi
|
||||
if [ -n "$KILO_CONFIG_DIR" ]; then
|
||||
ENV_RUNTIME_DIRS+=( "kilo:$(expand_home "$KILO_CONFIG_DIR")" )
|
||||
elif [ -n "$XDG_CONFIG_HOME" ]; then
|
||||
ENV_RUNTIME_DIRS+=( "kilo:$(expand_home "$XDG_CONFIG_HOME")/kilo" )
|
||||
fi
|
||||
if [ -n "$OPENCODE_CONFIG_DIR" ]; then
|
||||
ENV_RUNTIME_DIRS+=( "opencode:$(expand_home "$OPENCODE_CONFIG_DIR")" )
|
||||
elif [ -n "$OPENCODE_CONFIG" ]; then
|
||||
ENV_RUNTIME_DIRS+=( "opencode:$(dirname "$(expand_home "$OPENCODE_CONFIG")")" )
|
||||
elif [ -n "$XDG_CONFIG_HOME" ]; then
|
||||
ENV_RUNTIME_DIRS+=( "opencode:$(expand_home "$XDG_CONFIG_HOME")/opencode" )
|
||||
fi
|
||||
if [ -n "$CODEX_HOME" ]; then
|
||||
ENV_RUNTIME_DIRS+=( "codex:$(expand_home "$CODEX_HOME")" )
|
||||
fi
|
||||
|
||||
# Reorder entries so preferred runtime is checked first.
|
||||
ORDERED_RUNTIME_DIRS=()
|
||||
for entry in "${RUNTIME_DIRS[@]}"; do
|
||||
@@ -50,6 +125,19 @@ for entry in "${RUNTIME_DIRS[@]}"; do
|
||||
ORDERED_RUNTIME_DIRS+=( "$entry" )
|
||||
fi
|
||||
done
|
||||
ORDERED_ENV_RUNTIME_DIRS=()
|
||||
for entry in "${ENV_RUNTIME_DIRS[@]}"; do
|
||||
runtime="${entry%%:*}"
|
||||
if [ "$runtime" = "$PREFERRED_RUNTIME" ]; then
|
||||
ORDERED_ENV_RUNTIME_DIRS+=( "$entry" )
|
||||
fi
|
||||
done
|
||||
for entry in "${ENV_RUNTIME_DIRS[@]}"; do
|
||||
runtime="${entry%%:*}"
|
||||
if [ "$runtime" != "$PREFERRED_RUNTIME" ]; then
|
||||
ORDERED_ENV_RUNTIME_DIRS+=( "$entry" )
|
||||
fi
|
||||
done
|
||||
for entry in "${RUNTIME_DIRS[@]}"; do
|
||||
runtime="${entry%%:*}"
|
||||
if [ "$runtime" != "$PREFERRED_RUNTIME" ]; then
|
||||
@@ -72,18 +160,32 @@ for entry in "${ORDERED_RUNTIME_DIRS[@]}"; do
|
||||
done
|
||||
|
||||
GLOBAL_VERSION_FILE="" GLOBAL_MARKER_FILE="" GLOBAL_DIR="" GLOBAL_RUNTIME=""
|
||||
for entry in "${ORDERED_RUNTIME_DIRS[@]}"; do
|
||||
for entry in "${ORDERED_ENV_RUNTIME_DIRS[@]}"; do
|
||||
runtime="${entry%%:*}"
|
||||
dir="${entry#*:}"
|
||||
if [ -f "$HOME/$dir/get-shit-done/VERSION" ] || [ -f "$HOME/$dir/get-shit-done/workflows/update.md" ]; then
|
||||
if [ -f "$dir/get-shit-done/VERSION" ] || [ -f "$dir/get-shit-done/workflows/update.md" ]; then
|
||||
GLOBAL_RUNTIME="$runtime"
|
||||
GLOBAL_VERSION_FILE="$HOME/$dir/get-shit-done/VERSION"
|
||||
GLOBAL_MARKER_FILE="$HOME/$dir/get-shit-done/workflows/update.md"
|
||||
GLOBAL_DIR="$(cd "$HOME/$dir" 2>/dev/null && pwd)"
|
||||
GLOBAL_VERSION_FILE="$dir/get-shit-done/VERSION"
|
||||
GLOBAL_MARKER_FILE="$dir/get-shit-done/workflows/update.md"
|
||||
GLOBAL_DIR="$(cd "$dir" 2>/dev/null && pwd)"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "$GLOBAL_RUNTIME" ]; then
|
||||
for entry in "${ORDERED_RUNTIME_DIRS[@]}"; do
|
||||
runtime="${entry%%:*}"
|
||||
dir="${entry#*:}"
|
||||
if [ -f "$HOME/$dir/get-shit-done/VERSION" ] || [ -f "$HOME/$dir/get-shit-done/workflows/update.md" ]; then
|
||||
GLOBAL_RUNTIME="$runtime"
|
||||
GLOBAL_VERSION_FILE="$HOME/$dir/get-shit-done/VERSION"
|
||||
GLOBAL_MARKER_FILE="$HOME/$dir/get-shit-done/workflows/update.md"
|
||||
GLOBAL_DIR="$(cd "$HOME/$dir" 2>/dev/null && pwd)"
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# Only treat as LOCAL if the resolved paths differ (prevents misdetection when CWD=$HOME)
|
||||
IS_LOCAL=false
|
||||
if [ -n "$LOCAL_VERSION_FILE" ] && [ -f "$LOCAL_VERSION_FILE" ] && [ -f "$LOCAL_MARKER_FILE" ] && grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+' "$LOCAL_VERSION_FILE"; then
|
||||
@@ -123,7 +225,7 @@ echo "$TARGET_RUNTIME"
|
||||
Parse output:
|
||||
- Line 1 = installed version (`0.0.0` means unknown version)
|
||||
- Line 2 = install scope (`LOCAL`, `GLOBAL`, or `UNKNOWN`)
|
||||
- Line 3 = target runtime (`claude`, `opencode`, `gemini`, or `codex`)
|
||||
- Line 3 = target runtime (`claude`, `opencode`, `gemini`, `kilo`, or `codex`)
|
||||
- If scope is `UNKNOWN`, proceed to install step using `--claude --global` fallback.
|
||||
|
||||
If multiple runtime installs are detected and the invoking runtime cannot be determined from execution_context, ask the user which runtime to update before running install.
|
||||
@@ -221,8 +323,8 @@ Exit.
|
||||
- `agents/gsd-*` files will be replaced
|
||||
|
||||
(Paths are relative to detected runtime install location:
|
||||
global: `~/.claude/`, `~/.config/opencode/`, `~/.opencode/`, `~/.gemini/`, or `~/.codex/`
|
||||
local: `./.claude/`, `./.config/opencode/`, `./.opencode/`, `./.gemini/`, or `./.codex/`)
|
||||
global: `~/.claude/`, `~/.config/opencode/`, `~/.opencode/`, `~/.gemini/`, `~/.config/kilo/`, or `~/.codex/`
|
||||
local: `./.claude/`, `./.config/opencode/`, `./.opencode/`, `./.gemini/`, `./.kilo/`, or `./.codex/`)
|
||||
|
||||
Your custom files in other locations are preserved:
|
||||
- Custom commands not in `commands/gsd/` ✓
|
||||
@@ -270,14 +372,53 @@ Capture output. If install fails, show error and exit.
|
||||
Clear the update cache so statusline indicator disappears:
|
||||
|
||||
```bash
|
||||
# Clear update cache across all runtime directories
|
||||
for dir in .claude .config/opencode .opencode .gemini .codex; do
|
||||
expand_home() {
|
||||
case "$1" in
|
||||
"~/"*) printf '%s/%s\n' "$HOME" "${1#~/}" ;;
|
||||
*) printf '%s\n' "$1" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Clear update cache across preferred, env-derived, and default runtime directories
|
||||
CACHE_DIRS=()
|
||||
if [ -n "$PREFERRED_CONFIG_DIR" ]; then
|
||||
CACHE_DIRS+=( "$(expand_home "$PREFERRED_CONFIG_DIR")" )
|
||||
fi
|
||||
if [ -n "$CLAUDE_CONFIG_DIR" ]; then
|
||||
CACHE_DIRS+=( "$(expand_home "$CLAUDE_CONFIG_DIR")" )
|
||||
fi
|
||||
if [ -n "$GEMINI_CONFIG_DIR" ]; then
|
||||
CACHE_DIRS+=( "$(expand_home "$GEMINI_CONFIG_DIR")" )
|
||||
fi
|
||||
if [ -n "$KILO_CONFIG_DIR" ]; then
|
||||
CACHE_DIRS+=( "$(expand_home "$KILO_CONFIG_DIR")" )
|
||||
elif [ -n "$XDG_CONFIG_HOME" ]; then
|
||||
CACHE_DIRS+=( "$(expand_home "$XDG_CONFIG_HOME")/kilo" )
|
||||
fi
|
||||
if [ -n "$OPENCODE_CONFIG_DIR" ]; then
|
||||
CACHE_DIRS+=( "$(expand_home "$OPENCODE_CONFIG_DIR")" )
|
||||
elif [ -n "$OPENCODE_CONFIG" ]; then
|
||||
CACHE_DIRS+=( "$(dirname "$(expand_home "$OPENCODE_CONFIG")")" )
|
||||
elif [ -n "$XDG_CONFIG_HOME" ]; then
|
||||
CACHE_DIRS+=( "$(expand_home "$XDG_CONFIG_HOME")/opencode" )
|
||||
fi
|
||||
if [ -n "$CODEX_HOME" ]; then
|
||||
CACHE_DIRS+=( "$(expand_home "$CODEX_HOME")" )
|
||||
fi
|
||||
|
||||
for dir in "${CACHE_DIRS[@]}"; do
|
||||
if [ -n "$dir" ]; then
|
||||
rm -f "$dir/cache/gsd-update-check.json"
|
||||
fi
|
||||
done
|
||||
|
||||
for dir in .claude .config/opencode .opencode .gemini .config/kilo .kilo .codex; do
|
||||
rm -f "./$dir/cache/gsd-update-check.json"
|
||||
rm -f "$HOME/$dir/cache/gsd-update-check.json"
|
||||
done
|
||||
```
|
||||
|
||||
The SessionStart hook (`gsd-check-update.js`) writes to the detected runtime's cache directory, so all paths must be cleared to prevent stale update indicators.
|
||||
The SessionStart hook (`gsd-check-update.js`) writes to the detected runtime's cache directory, so preferred/env-derived paths and default paths must all be cleared to prevent stale update indicators.
|
||||
</step>
|
||||
|
||||
<step name="display_result">
|
||||
|
||||
@@ -11,7 +11,7 @@ const { spawn } = require('child_process');
|
||||
const homeDir = os.homedir();
|
||||
const cwd = process.cwd();
|
||||
|
||||
// Detect runtime config directory (supports Claude, OpenCode, Gemini)
|
||||
// Detect runtime config directory (supports Claude, OpenCode, Kilo, Gemini)
|
||||
// Respects CLAUDE_CONFIG_DIR for custom config directory setups
|
||||
function detectConfigDir(baseDir) {
|
||||
// Check env override first (supports multi-account setups)
|
||||
@@ -19,7 +19,7 @@ function detectConfigDir(baseDir) {
|
||||
if (envDir && fs.existsSync(path.join(envDir, 'get-shit-done', 'VERSION'))) {
|
||||
return envDir;
|
||||
}
|
||||
for (const dir of ['.config/opencode', '.opencode', '.gemini', '.claude']) {
|
||||
for (const dir of ['.config/kilo', '.kilo', '.config/opencode', '.opencode', '.gemini', '.claude']) {
|
||||
if (fs.existsSync(path.join(baseDir, dir, 'get-shit-done', 'VERSION'))) {
|
||||
return path.join(baseDir, dir);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { GSDTools, GSDToolsError, resolveGsdToolsPath } from './gsd-tools.js';
|
||||
import { mkdir, writeFile, rm } from 'node:fs/promises';
|
||||
import { existsSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { tmpdir, homedir } from 'node:os';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const BUNDLED_GSD_TOOLS_PATH = fileURLToPath(
|
||||
new URL('../../get-shit-done/bin/gsd-tools.cjs', import.meta.url),
|
||||
);
|
||||
|
||||
describe('GSDTools', () => {
|
||||
let tmpDir: string;
|
||||
@@ -330,11 +336,13 @@ describe('GSDTools', () => {
|
||||
expect(result).toBe(join(localBinDir, 'gsd-tools.cjs'));
|
||||
});
|
||||
|
||||
it('falls back to global path when repo-local does not exist', () => {
|
||||
it('falls back to bundled repo path when repo-local does not exist', () => {
|
||||
const result = resolveGsdToolsPath(tmpDir);
|
||||
expect(result).toBe(
|
||||
join(homedir(), '.claude', 'get-shit-done', 'bin', 'gsd-tools.cjs'),
|
||||
);
|
||||
const expected = existsSync(BUNDLED_GSD_TOOLS_PATH)
|
||||
? BUNDLED_GSD_TOOLS_PATH
|
||||
: join(homedir(), '.claude', 'get-shit-done', 'bin', 'gsd-tools.cjs');
|
||||
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
|
||||
it('constructor uses repo-local path when available', async () => {
|
||||
|
||||
@@ -10,6 +10,7 @@ import { readFile } from 'node:fs/promises';
|
||||
import { existsSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { homedir } from 'node:os';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import type { InitNewProjectInfo, PhaseOpInfo, PhasePlanIndex, RoadmapAnalysis } from './types.js';
|
||||
|
||||
// ─── Error type ──────────────────────────────────────────────────────────────
|
||||
@@ -30,6 +31,9 @@ export class GSDToolsError extends Error {
|
||||
// ─── GSDTools class ──────────────────────────────────────────────────────────
|
||||
|
||||
const DEFAULT_TIMEOUT_MS = 30_000;
|
||||
const BUNDLED_GSD_TOOLS_PATH = fileURLToPath(
|
||||
new URL('../../get-shit-done/bin/gsd-tools.cjs', import.meta.url),
|
||||
);
|
||||
|
||||
export class GSDTools {
|
||||
private readonly projectDir: string;
|
||||
@@ -58,7 +62,7 @@ export class GSDTools {
|
||||
|
||||
return new Promise<unknown>((resolve, reject) => {
|
||||
const child = execFile(
|
||||
'node',
|
||||
process.execPath,
|
||||
fullArgs,
|
||||
{
|
||||
cwd: this.projectDir,
|
||||
@@ -160,7 +164,7 @@ export class GSDTools {
|
||||
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
const child = execFile(
|
||||
'node',
|
||||
process.execPath,
|
||||
fullArgs,
|
||||
{
|
||||
cwd: this.projectDir,
|
||||
@@ -285,11 +289,15 @@ export class GSDTools {
|
||||
// ─── Path resolution ────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Resolve gsd-tools.cjs path with repo-local fallback.
|
||||
* Probe order: repo-local → global home directory.
|
||||
* Resolve gsd-tools.cjs path with bundled-repo fallback.
|
||||
* Probe order: project-local → repo-bundled → global home directory.
|
||||
*/
|
||||
export function resolveGsdToolsPath(projectDir: string): string {
|
||||
const localPath = join(projectDir, '.claude', 'get-shit-done', 'bin', 'gsd-tools.cjs');
|
||||
if (existsSync(localPath)) return localPath;
|
||||
return join(homedir(), '.claude', 'get-shit-done', 'bin', 'gsd-tools.cjs');
|
||||
const candidates = [
|
||||
join(projectDir, '.claude', 'get-shit-done', 'bin', 'gsd-tools.cjs'),
|
||||
BUNDLED_GSD_TOOLS_PATH,
|
||||
join(homedir(), '.claude', 'get-shit-done', 'bin', 'gsd-tools.cjs'),
|
||||
];
|
||||
|
||||
return candidates.find(candidate => existsSync(candidate)) ?? candidates[candidates.length - 1]!;
|
||||
}
|
||||
|
||||
@@ -13,13 +13,13 @@
|
||||
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
||||
import { execSync } from 'node:child_process';
|
||||
import { mkdtemp, rm, readFile, stat } from 'node:fs/promises';
|
||||
import { existsSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { homedir } from 'node:os';
|
||||
|
||||
import { InitRunner } from './init-runner.js';
|
||||
import { GSDTools } from './gsd-tools.js';
|
||||
import { GSDTools, resolveGsdToolsPath } from './gsd-tools.js';
|
||||
import { GSDEventStream } from './event-stream.js';
|
||||
import { GSDEventType } from './types.js';
|
||||
import type { GSDEvent } from './types.js';
|
||||
@@ -36,11 +36,12 @@ try {
|
||||
|
||||
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
||||
const sdkPromptsDir = join(__dirname, '..', 'prompts');
|
||||
const GSD_TOOLS_PATH = join(homedir(), '.claude', 'get-shit-done', 'bin', 'gsd-tools.cjs');
|
||||
const GSD_TOOLS_PATH = resolveGsdToolsPath(process.cwd());
|
||||
const gsdToolsAvailable = existsSync(GSD_TOOLS_PATH);
|
||||
|
||||
// ─── Test suite ──────────────────────────────────────────────────────────────
|
||||
|
||||
describe.skipIf(!cliAvailable)('E2E: InitRunner.run() full workflow', () => {
|
||||
describe.skipIf(!cliAvailable || !gsdToolsAvailable)('E2E: InitRunner.run() full workflow', () => {
|
||||
let tmpDir: string;
|
||||
let events: GSDEvent[];
|
||||
|
||||
|
||||
@@ -14,14 +14,14 @@
|
||||
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
||||
import { execSync } from 'node:child_process';
|
||||
import { mkdtemp, rm, readFile, stat, readdir } from 'node:fs/promises';
|
||||
import { existsSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { homedir } from 'node:os';
|
||||
|
||||
import { GSD } from './index.js';
|
||||
import { InitRunner } from './init-runner.js';
|
||||
import { GSDTools } from './gsd-tools.js';
|
||||
import { GSDTools, resolveGsdToolsPath } from './gsd-tools.js';
|
||||
import { GSDEventStream } from './event-stream.js';
|
||||
import { GSDEventType, PhaseStepType } from './types.js';
|
||||
import type { GSDEvent, PhaseRunnerResult, RoadmapAnalysis } from './types.js';
|
||||
@@ -38,7 +38,8 @@ try {
|
||||
|
||||
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
||||
const sdkPromptsDir = join(__dirname, '..', 'prompts');
|
||||
const GSD_TOOLS_PATH = join(homedir(), '.claude', 'get-shit-done', 'bin', 'gsd-tools.cjs');
|
||||
const GSD_TOOLS_PATH = resolveGsdToolsPath(process.cwd());
|
||||
const gsdToolsAvailable = existsSync(GSD_TOOLS_PATH);
|
||||
|
||||
// ─── Lifecycle step ordering for monotonicity check ──────────────────────────
|
||||
|
||||
@@ -54,7 +55,7 @@ const STEP_ORDER: Record<string, number> = {
|
||||
|
||||
// ─── Test suite ──────────────────────────────────────────────────────────────
|
||||
|
||||
describe.skipIf(!cliAvailable)('E2E Lifecycle: InitRunner → GSD.runPhase() full lifecycle', () => {
|
||||
describe.skipIf(!cliAvailable || !gsdToolsAvailable)('E2E Lifecycle: InitRunner → GSD.runPhase() full lifecycle', () => {
|
||||
let tmpDir: string;
|
||||
let initSuccess: boolean = false;
|
||||
let phase1Number: string | null = null;
|
||||
|
||||
@@ -8,11 +8,11 @@
|
||||
|
||||
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
||||
import { mkdtemp, mkdir, writeFile, rm } from 'node:fs/promises';
|
||||
import { existsSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { homedir } from 'node:os';
|
||||
|
||||
import { GSDTools } from './gsd-tools.js';
|
||||
import { GSDTools, resolveGsdToolsPath } from './gsd-tools.js';
|
||||
import { PhaseRunner } from './phase-runner.js';
|
||||
import type { PhaseRunnerDeps } from './phase-runner.js';
|
||||
import { ContextEngine } from './context-engine.js';
|
||||
@@ -24,7 +24,8 @@ import { GSDEventType, PhaseStepType } from './types.js';
|
||||
|
||||
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
||||
|
||||
const GSD_TOOLS_PATH = join(homedir(), '.claude', 'get-shit-done', 'bin', 'gsd-tools.cjs');
|
||||
const GSD_TOOLS_PATH = resolveGsdToolsPath(process.cwd());
|
||||
const gsdToolsAvailable = existsSync(GSD_TOOLS_PATH);
|
||||
|
||||
async function createTempPlanningDir(): Promise<string> {
|
||||
const tmpDir = await mkdtemp(join(tmpdir(), 'gsd-sdk-phase-int-'));
|
||||
@@ -65,7 +66,7 @@ async function createTempPlanningDir(): Promise<string> {
|
||||
|
||||
// ─── Test suite ──────────────────────────────────────────────────────────────
|
||||
|
||||
describe('Integration: PhaseRunner against real gsd-tools.cjs', () => {
|
||||
describe.skipIf(!gsdToolsAvailable)('Integration: PhaseRunner against real gsd-tools.cjs', () => {
|
||||
let tmpDir: string;
|
||||
let tools: GSDTools;
|
||||
|
||||
@@ -303,7 +304,7 @@ must_haves:
|
||||
return tmpDir;
|
||||
}
|
||||
|
||||
describe('Integration: phasePlanIndex and wave execution', () => {
|
||||
describe.skipIf(!gsdToolsAvailable)('Integration: phasePlanIndex and wave execution', () => {
|
||||
let tmpDir: string;
|
||||
let tools: GSDTools;
|
||||
|
||||
|
||||
@@ -197,7 +197,7 @@ export interface PlanResult {
|
||||
export interface GSDOptions {
|
||||
/** Root directory of the project. */
|
||||
projectDir: string;
|
||||
/** Path to gsd-tools.cjs. Falls back to <projectDir>/.claude/ then ~/.claude/. */
|
||||
/** Path to gsd-tools.cjs. Falls back to <projectDir>/.claude/, then the bundled repo path, then ~/.claude/. */
|
||||
gsdToolsPath?: string;
|
||||
/** Model to use for execution sessions. */
|
||||
model?: string;
|
||||
|
||||
@@ -36,6 +36,7 @@ describe('getDirName (Antigravity)', () => {
|
||||
assert.strictEqual(getDirName('claude'), '.claude');
|
||||
assert.strictEqual(getDirName('opencode'), '.opencode');
|
||||
assert.strictEqual(getDirName('gemini'), '.gemini');
|
||||
assert.strictEqual(getDirName('kilo'), '.kilo');
|
||||
assert.strictEqual(getDirName('codex'), '.codex');
|
||||
assert.strictEqual(getDirName('copilot'), '.github');
|
||||
});
|
||||
@@ -97,6 +98,7 @@ describe('getConfigDirFromHome (Antigravity)', () => {
|
||||
test('does not change other runtimes', () => {
|
||||
assert.strictEqual(getConfigDirFromHome('claude', true), "'.claude'");
|
||||
assert.strictEqual(getConfigDirFromHome('gemini', true), "'.gemini'");
|
||||
assert.strictEqual(getConfigDirFromHome('kilo', true), "'.config', 'kilo'");
|
||||
assert.strictEqual(getConfigDirFromHome('copilot', true), "'.copilot'");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -44,6 +44,7 @@ describe('getDirName (Copilot)', () => {
|
||||
assert.strictEqual(getDirName('claude'), '.claude');
|
||||
assert.strictEqual(getDirName('opencode'), '.opencode');
|
||||
assert.strictEqual(getDirName('gemini'), '.gemini');
|
||||
assert.strictEqual(getDirName('kilo'), '.kilo');
|
||||
assert.strictEqual(getDirName('codex'), '.codex');
|
||||
});
|
||||
});
|
||||
@@ -109,6 +110,7 @@ describe('getConfigDirFromHome (Copilot)', () => {
|
||||
assert.strictEqual(getConfigDirFromHome('opencode', true), "'.config', 'opencode'");
|
||||
assert.strictEqual(getConfigDirFromHome('claude', true), "'.claude'");
|
||||
assert.strictEqual(getConfigDirFromHome('gemini', true), "'.gemini'");
|
||||
assert.strictEqual(getConfigDirFromHome('kilo', true), "'.config', 'kilo'");
|
||||
assert.strictEqual(getConfigDirFromHome('codex', true), "'.codex'");
|
||||
});
|
||||
});
|
||||
@@ -137,8 +139,8 @@ describe('Source code integration (Copilot)', () => {
|
||||
assert.ok(src.includes('--copilot'), 'help text has --copilot option');
|
||||
});
|
||||
|
||||
test('CLI-02: promptRuntime runtimeMap has Copilot as option 5', () => {
|
||||
assert.ok(src.includes("'5': 'copilot'"), 'runtimeMap has 5 -> copilot');
|
||||
test('CLI-02: promptRuntime runtimeMap has Copilot as option 6', () => {
|
||||
assert.ok(src.includes("'6': 'copilot'"), 'runtimeMap has 6 -> copilot');
|
||||
});
|
||||
|
||||
test('CLI-02: promptRuntime allRuntimes array includes copilot', () => {
|
||||
|
||||
252
tests/kilo-install.test.cjs
Normal file
252
tests/kilo-install.test.cjs
Normal file
@@ -0,0 +1,252 @@
|
||||
/**
|
||||
* GSD Tools Tests - Kilo Install Plumbing
|
||||
*
|
||||
* Tests for Kilo runtime directory resolution, config paths,
|
||||
* permission config, and installer source integration.
|
||||
*/
|
||||
|
||||
process.env.GSD_TEST_MODE = '1';
|
||||
|
||||
const { test, describe, beforeEach, afterEach } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const os = require('node:os');
|
||||
|
||||
const { createTempProject, cleanup } = require('./helpers.cjs');
|
||||
const {
|
||||
getDirName,
|
||||
getGlobalDir,
|
||||
getConfigDirFromHome,
|
||||
resolveKiloConfigPath,
|
||||
configureKiloPermissions,
|
||||
} = require('../bin/install.js');
|
||||
|
||||
describe('getDirName (Kilo)', () => {
|
||||
test('returns .kilo for kilo', () => {
|
||||
assert.strictEqual(getDirName('kilo'), '.kilo');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getConfigDirFromHome (Kilo)', () => {
|
||||
test('returns .kilo for local installs', () => {
|
||||
assert.strictEqual(getConfigDirFromHome('kilo', false), "'.kilo'");
|
||||
});
|
||||
|
||||
test('returns .config, kilo for global installs', () => {
|
||||
assert.strictEqual(getConfigDirFromHome('kilo', true), "'.config', 'kilo'");
|
||||
});
|
||||
});
|
||||
|
||||
describe('getGlobalDir (Kilo)', () => {
|
||||
let savedEnv;
|
||||
|
||||
beforeEach(() => {
|
||||
savedEnv = {
|
||||
KILO_CONFIG_DIR: process.env.KILO_CONFIG_DIR,
|
||||
KILO_CONFIG: process.env.KILO_CONFIG,
|
||||
XDG_CONFIG_HOME: process.env.XDG_CONFIG_HOME,
|
||||
};
|
||||
|
||||
delete process.env.KILO_CONFIG_DIR;
|
||||
delete process.env.KILO_CONFIG;
|
||||
delete process.env.XDG_CONFIG_HOME;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (savedEnv.KILO_CONFIG_DIR === undefined) {
|
||||
delete process.env.KILO_CONFIG_DIR;
|
||||
} else {
|
||||
process.env.KILO_CONFIG_DIR = savedEnv.KILO_CONFIG_DIR;
|
||||
}
|
||||
|
||||
if (savedEnv.KILO_CONFIG === undefined) {
|
||||
delete process.env.KILO_CONFIG;
|
||||
} else {
|
||||
process.env.KILO_CONFIG = savedEnv.KILO_CONFIG;
|
||||
}
|
||||
|
||||
if (savedEnv.XDG_CONFIG_HOME === undefined) {
|
||||
delete process.env.XDG_CONFIG_HOME;
|
||||
} else {
|
||||
process.env.XDG_CONFIG_HOME = savedEnv.XDG_CONFIG_HOME;
|
||||
}
|
||||
});
|
||||
|
||||
test('returns ~/.config/kilo by default', () => {
|
||||
assert.strictEqual(getGlobalDir('kilo'), path.join(os.homedir(), '.config', 'kilo'));
|
||||
});
|
||||
|
||||
test('respects KILO_CONFIG_DIR env var', () => {
|
||||
process.env.KILO_CONFIG_DIR = '~/custom-kilo';
|
||||
assert.strictEqual(getGlobalDir('kilo'), path.join(os.homedir(), 'custom-kilo'));
|
||||
});
|
||||
|
||||
test('falls back to XDG_CONFIG_HOME/kilo', () => {
|
||||
process.env.XDG_CONFIG_HOME = '~/xdg-config';
|
||||
assert.strictEqual(getGlobalDir('kilo'), path.join(os.homedir(), 'xdg-config', 'kilo'));
|
||||
});
|
||||
|
||||
test('ignores KILO_CONFIG file env var and uses XDG/default resolution only', () => {
|
||||
process.env.KILO_CONFIG = '/tmp/ignored-kilo.json';
|
||||
assert.strictEqual(getGlobalDir('kilo'), path.join(os.homedir(), '.config', 'kilo'));
|
||||
});
|
||||
|
||||
test('explicit config-dir overrides env vars', () => {
|
||||
process.env.KILO_CONFIG_DIR = '~/from-env';
|
||||
process.env.XDG_CONFIG_HOME = '~/xdg-config';
|
||||
assert.strictEqual(getGlobalDir('kilo', '/explicit/kilo'), '/explicit/kilo');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Kilo config file helpers', () => {
|
||||
let tmpDir;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = createTempProject('gsd-kilo-');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
|
||||
test('resolveKiloConfigPath prefers kilo.jsonc when present', () => {
|
||||
const configDir = path.join(tmpDir, '.kilo');
|
||||
fs.mkdirSync(configDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(configDir, 'kilo.jsonc'), '{\n // comment\n}\n');
|
||||
|
||||
assert.strictEqual(resolveKiloConfigPath(configDir), path.join(configDir, 'kilo.jsonc'));
|
||||
});
|
||||
|
||||
test('resolveKiloConfigPath falls back to kilo.json', () => {
|
||||
const configDir = path.join(tmpDir, '.kilo');
|
||||
fs.mkdirSync(configDir, { recursive: true });
|
||||
|
||||
assert.strictEqual(resolveKiloConfigPath(configDir), path.join(configDir, 'kilo.json'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('configureKiloPermissions', () => {
|
||||
let tmpDir;
|
||||
let configDir;
|
||||
let savedEnv;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = createTempProject('gsd-kilo-perms-');
|
||||
configDir = path.join(tmpDir, '.config', 'kilo');
|
||||
savedEnv = {
|
||||
KILO_CONFIG_DIR: process.env.KILO_CONFIG_DIR,
|
||||
XDG_CONFIG_HOME: process.env.XDG_CONFIG_HOME,
|
||||
};
|
||||
process.env.KILO_CONFIG_DIR = configDir;
|
||||
delete process.env.XDG_CONFIG_HOME;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (savedEnv.KILO_CONFIG_DIR === undefined) {
|
||||
delete process.env.KILO_CONFIG_DIR;
|
||||
} else {
|
||||
process.env.KILO_CONFIG_DIR = savedEnv.KILO_CONFIG_DIR;
|
||||
}
|
||||
|
||||
if (savedEnv.XDG_CONFIG_HOME === undefined) {
|
||||
delete process.env.XDG_CONFIG_HOME;
|
||||
} else {
|
||||
process.env.XDG_CONFIG_HOME = savedEnv.XDG_CONFIG_HOME;
|
||||
}
|
||||
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
|
||||
test('writes GSD permissions to kilo.json when config is missing', () => {
|
||||
configureKiloPermissions(true);
|
||||
|
||||
const configPath = path.join(configDir, 'kilo.json');
|
||||
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
||||
const gsdPath = `${configDir.replace(/\\/g, '/')}/get-shit-done/*`;
|
||||
|
||||
assert.strictEqual(config.permission.read[gsdPath], 'allow');
|
||||
assert.strictEqual(config.permission.external_directory[gsdPath], 'allow');
|
||||
});
|
||||
|
||||
test('updates existing kilo.jsonc configs via JSONC parsing', () => {
|
||||
fs.mkdirSync(configDir, { recursive: true });
|
||||
const configPath = path.join(configDir, 'kilo.jsonc');
|
||||
fs.writeFileSync(configPath, '{\n // existing config\n "permission": {\n "bash": "ask",\n },\n}\n');
|
||||
|
||||
configureKiloPermissions(true);
|
||||
|
||||
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
||||
const gsdPath = `${configDir.replace(/\\/g, '/')}/get-shit-done/*`;
|
||||
|
||||
assert.strictEqual(config.permission.bash, 'ask');
|
||||
assert.strictEqual(config.permission.read[gsdPath], 'allow');
|
||||
assert.strictEqual(config.permission.external_directory[gsdPath], 'allow');
|
||||
});
|
||||
|
||||
test('writes permissions to an explicit config dir argument', () => {
|
||||
const explicitDir = path.join(tmpDir, 'custom-kilo-config');
|
||||
|
||||
configureKiloPermissions(true, explicitDir);
|
||||
|
||||
const configPath = path.join(explicitDir, 'kilo.json');
|
||||
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
||||
const gsdPath = `${explicitDir.replace(/\\/g, '/')}/get-shit-done/*`;
|
||||
|
||||
assert.strictEqual(config.permission.read[gsdPath], 'allow');
|
||||
assert.strictEqual(config.permission.external_directory[gsdPath], 'allow');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Source code integration (Kilo)', () => {
|
||||
const src = fs.readFileSync(path.join(__dirname, '..', 'bin', 'install.js'), 'utf8');
|
||||
const updateWorkflowSrc = fs.readFileSync(path.join(__dirname, '..', 'get-shit-done', 'workflows', 'update.md'), 'utf8');
|
||||
const reapplyPatchesSrc = fs.readFileSync(path.join(__dirname, '..', 'commands', 'gsd', 'reapply-patches.md'), 'utf8');
|
||||
|
||||
test('--kilo flag parsing exists', () => {
|
||||
assert.ok(src.includes("args.includes('--kilo')"), '--kilo flag parsed');
|
||||
});
|
||||
|
||||
test('help text includes --kilo', () => {
|
||||
assert.ok(src.includes('Install for Kilo only'), 'help text includes Kilo option');
|
||||
});
|
||||
|
||||
test('--all array includes kilo', () => {
|
||||
assert.ok(src.includes("'kilo'"), '--all includes kilo runtime');
|
||||
});
|
||||
|
||||
test('promptRuntime runtimeMap has Kilo as option 4', () => {
|
||||
assert.ok(src.includes("'4': 'kilo'"), 'runtimeMap has 4 -> kilo');
|
||||
});
|
||||
|
||||
test('hooks are skipped for Kilo', () => {
|
||||
assert.ok(src.includes('!isOpencode && !isKilo'), 'hooks skip check includes kilo');
|
||||
});
|
||||
|
||||
test('settings.json write excludes Kilo', () => {
|
||||
assert.ok(src.includes('!isCodex && !isCopilot && !isKilo && !isCursor && !isWindsurf'), 'settings write excludes kilo');
|
||||
});
|
||||
|
||||
test('agent path replacement does not exclude Kilo', () => {
|
||||
assert.ok(src.includes('if (!isCopilot && !isAntigravity)'), 'generic agent path replacement still applies');
|
||||
});
|
||||
|
||||
test('finishInstall passes the actual config dir to Kilo permissions', () => {
|
||||
assert.ok(src.includes('configureKiloPermissions(isGlobal, configDir);'), 'Kilo permission config uses actual install dir');
|
||||
});
|
||||
|
||||
test('uninstall cleans Kilo permissions from the resolved target dir', () => {
|
||||
assert.ok(src.includes('const configPath = resolveKiloConfigPath(targetDir);'), 'Kilo uninstall cleanup uses targetDir');
|
||||
});
|
||||
|
||||
test('update workflow checks preferred custom config dirs before defaults', () => {
|
||||
assert.ok(updateWorkflowSrc.includes('PREFERRED_CONFIG_DIR'), 'workflow tracks preferred config dir');
|
||||
assert.ok(updateWorkflowSrc.includes('kilo.jsonc'), 'workflow infers Kilo from config files');
|
||||
assert.ok(updateWorkflowSrc.includes('ENV_RUNTIME_DIRS'), 'workflow checks env-derived config dirs');
|
||||
});
|
||||
|
||||
test('reapply-patches checks Kilo custom config env vars first', () => {
|
||||
assert.ok(reapplyPatchesSrc.includes('KILO_CONFIG_DIR'), 'reapply-patches checks KILO_CONFIG_DIR');
|
||||
assert.ok(reapplyPatchesSrc.includes('XDG_CONFIG_HOME'), 'reapply-patches checks XDG_CONFIG_HOME');
|
||||
});
|
||||
});
|
||||
@@ -21,13 +21,14 @@ const runtimeMap = {
|
||||
'1': 'claude',
|
||||
'2': 'opencode',
|
||||
'3': 'gemini',
|
||||
'4': 'codex',
|
||||
'5': 'copilot',
|
||||
'6': 'antigravity',
|
||||
'7': 'cursor',
|
||||
'8': 'windsurf'
|
||||
'4': 'kilo',
|
||||
'5': 'codex',
|
||||
'6': 'copilot',
|
||||
'7': 'antigravity',
|
||||
'8': 'cursor',
|
||||
'9': 'windsurf'
|
||||
};
|
||||
const allRuntimes = ['claude', 'opencode', 'gemini', 'codex', 'copilot', 'antigravity', 'cursor', 'windsurf'];
|
||||
const allRuntimes = ['claude', 'opencode', 'gemini', 'kilo', 'codex', 'copilot', 'antigravity', 'cursor', 'windsurf'];
|
||||
|
||||
/**
|
||||
* Simulate the parsing logic from promptRuntime without requiring readline.
|
||||
@@ -36,7 +37,7 @@ const allRuntimes = ['claude', 'opencode', 'gemini', 'codex', 'copilot', 'antigr
|
||||
function parseRuntimeInput(input) {
|
||||
input = input.trim() || '1';
|
||||
|
||||
if (input === '9') {
|
||||
if (input === '10') {
|
||||
return allRuntimes;
|
||||
}
|
||||
|
||||
@@ -55,31 +56,33 @@ function parseRuntimeInput(input) {
|
||||
describe('multi-runtime selection parsing', () => {
|
||||
test('single choice returns single runtime', () => {
|
||||
assert.deepStrictEqual(parseRuntimeInput('1'), ['claude']);
|
||||
assert.deepStrictEqual(parseRuntimeInput('4'), ['codex']);
|
||||
assert.deepStrictEqual(parseRuntimeInput('7'), ['cursor']);
|
||||
assert.deepStrictEqual(parseRuntimeInput('4'), ['kilo']);
|
||||
assert.deepStrictEqual(parseRuntimeInput('5'), ['codex']);
|
||||
assert.deepStrictEqual(parseRuntimeInput('8'), ['cursor']);
|
||||
});
|
||||
|
||||
test('comma-separated choices return multiple runtimes', () => {
|
||||
assert.deepStrictEqual(parseRuntimeInput('1,4,6'), ['claude', 'codex', 'antigravity']);
|
||||
assert.deepStrictEqual(parseRuntimeInput('1,5,7'), ['claude', 'codex', 'antigravity']);
|
||||
assert.deepStrictEqual(parseRuntimeInput('2,3'), ['opencode', 'gemini']);
|
||||
assert.deepStrictEqual(parseRuntimeInput('2,4'), ['opencode', 'kilo']);
|
||||
});
|
||||
|
||||
test('space-separated choices return multiple runtimes', () => {
|
||||
assert.deepStrictEqual(parseRuntimeInput('1 4 6'), ['claude', 'codex', 'antigravity']);
|
||||
assert.deepStrictEqual(parseRuntimeInput('5 7'), ['copilot', 'cursor']);
|
||||
assert.deepStrictEqual(parseRuntimeInput('1 5 7'), ['claude', 'codex', 'antigravity']);
|
||||
assert.deepStrictEqual(parseRuntimeInput('6 8'), ['copilot', 'cursor']);
|
||||
});
|
||||
|
||||
test('mixed comma and space separators work', () => {
|
||||
assert.deepStrictEqual(parseRuntimeInput('1, 4, 6'), ['claude', 'codex', 'antigravity']);
|
||||
assert.deepStrictEqual(parseRuntimeInput('2 , 5'), ['opencode', 'copilot']);
|
||||
assert.deepStrictEqual(parseRuntimeInput('1, 5, 7'), ['claude', 'codex', 'antigravity']);
|
||||
assert.deepStrictEqual(parseRuntimeInput('2 , 6'), ['opencode', 'copilot']);
|
||||
});
|
||||
|
||||
test('single choice for windsurf', () => {
|
||||
assert.deepStrictEqual(parseRuntimeInput('8'), ['windsurf']);
|
||||
assert.deepStrictEqual(parseRuntimeInput('9'), ['windsurf']);
|
||||
});
|
||||
|
||||
test('choice 9 returns all runtimes', () => {
|
||||
assert.deepStrictEqual(parseRuntimeInput('9'), allRuntimes);
|
||||
test('choice 10 returns all runtimes', () => {
|
||||
assert.deepStrictEqual(parseRuntimeInput('10'), allRuntimes);
|
||||
});
|
||||
|
||||
test('empty input defaults to claude', () => {
|
||||
@@ -88,29 +91,29 @@ describe('multi-runtime selection parsing', () => {
|
||||
});
|
||||
|
||||
test('invalid choices are ignored, falls back to claude if all invalid', () => {
|
||||
assert.deepStrictEqual(parseRuntimeInput('10'), ['claude']);
|
||||
assert.deepStrictEqual(parseRuntimeInput('11'), ['claude']);
|
||||
assert.deepStrictEqual(parseRuntimeInput('0'), ['claude']);
|
||||
assert.deepStrictEqual(parseRuntimeInput('abc'), ['claude']);
|
||||
});
|
||||
|
||||
test('invalid choices mixed with valid are filtered out', () => {
|
||||
assert.deepStrictEqual(parseRuntimeInput('1,10,4'), ['claude', 'codex']);
|
||||
assert.deepStrictEqual(parseRuntimeInput('1,11,5'), ['claude', 'codex']);
|
||||
assert.deepStrictEqual(parseRuntimeInput('abc 3 xyz'), ['gemini']);
|
||||
});
|
||||
|
||||
test('duplicate choices are deduplicated', () => {
|
||||
assert.deepStrictEqual(parseRuntimeInput('1,1,1'), ['claude']);
|
||||
assert.deepStrictEqual(parseRuntimeInput('4,4,6,6'), ['codex', 'antigravity']);
|
||||
assert.deepStrictEqual(parseRuntimeInput('5,5,7,7'), ['codex', 'antigravity']);
|
||||
});
|
||||
|
||||
test('preserves selection order', () => {
|
||||
assert.deepStrictEqual(parseRuntimeInput('6,1,4'), ['antigravity', 'claude', 'codex']);
|
||||
assert.deepStrictEqual(parseRuntimeInput('7,2,5'), ['cursor', 'opencode', 'copilot']);
|
||||
assert.deepStrictEqual(parseRuntimeInput('7,1,5'), ['antigravity', 'claude', 'codex']);
|
||||
assert.deepStrictEqual(parseRuntimeInput('8,2,6'), ['cursor', 'opencode', 'copilot']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('install.js source contains multi-select support', () => {
|
||||
test('runtimeMap is defined with all 8 runtimes', () => {
|
||||
test('runtimeMap is defined with all 9 runtimes', () => {
|
||||
for (const [key, name] of Object.entries(runtimeMap)) {
|
||||
assert.ok(
|
||||
installSrc.includes(`'${key}': '${name}'`),
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
/**
|
||||
* Runtime Converter Tests — OpenCode + Gemini
|
||||
* Runtime Converter Tests — OpenCode + Kilo + Gemini
|
||||
*
|
||||
* Tests for small runtime-specific conversion functions from install.js.
|
||||
* Larger runtime test suites (Copilot, Codex, Antigravity) have their own files.
|
||||
*
|
||||
* OpenCode: convertClaudeToOpencodeFrontmatter (agent + command modes)
|
||||
* model: inherit is NOT added (OpenCode doesn't support it — see #1156)
|
||||
* but mode: subagent IS added (required by OpenCode agents).
|
||||
* OpenCode/Kilo: flat-runtime frontmatter converters (agent + command modes)
|
||||
* model: inherit is NOT added (runtime uses its configured default model)
|
||||
* but mode: subagent IS added (required by both runtimes' agents).
|
||||
* Gemini: convertClaudeToGeminiAgent (frontmatter + tool mapping + body escaping)
|
||||
*/
|
||||
|
||||
@@ -16,6 +16,7 @@ const assert = require('node:assert');
|
||||
process.env.GSD_TEST_MODE = '1';
|
||||
const {
|
||||
convertClaudeToOpencodeFrontmatter,
|
||||
convertClaudeToKiloFrontmatter,
|
||||
convertClaudeToGeminiAgent,
|
||||
neutralizeAgentReferences,
|
||||
} = require('../bin/install.js');
|
||||
@@ -52,66 +53,80 @@ allowed-tools:
|
||||
|
||||
Execute the phase plan.`;
|
||||
|
||||
describe('OpenCode agent conversion (isAgent: true)', () => {
|
||||
test('keeps name: field for agents', () => {
|
||||
const result = convertClaudeToOpencodeFrontmatter(SAMPLE_AGENT, { isAgent: true });
|
||||
const frontmatter = result.split('---')[1];
|
||||
assert.ok(frontmatter.includes('name: gsd-executor'), 'name: should be preserved for agents');
|
||||
});
|
||||
const flatRuntimeSuites = [
|
||||
{
|
||||
label: 'OpenCode',
|
||||
convert: convertClaudeToOpencodeFrontmatter,
|
||||
configDir: '.config/opencode',
|
||||
},
|
||||
{
|
||||
label: 'Kilo',
|
||||
convert: convertClaudeToKiloFrontmatter,
|
||||
configDir: '.config/kilo',
|
||||
},
|
||||
];
|
||||
|
||||
test('does not add model: inherit (OpenCode does not support it)', () => {
|
||||
const result = convertClaudeToOpencodeFrontmatter(SAMPLE_AGENT, { isAgent: true });
|
||||
const frontmatter = result.split('---')[1];
|
||||
assert.ok(!frontmatter.includes('model: inherit'), 'model: inherit should NOT be added — OpenCode throws ProviderModelNotFoundError');
|
||||
});
|
||||
for (const { label, convert, configDir } of flatRuntimeSuites) {
|
||||
describe(`${label} agent conversion (isAgent: true)`, () => {
|
||||
test('keeps name: field for agents', () => {
|
||||
const result = convert(SAMPLE_AGENT, { isAgent: true });
|
||||
const frontmatter = result.split('---')[1];
|
||||
assert.ok(frontmatter.includes('name: gsd-executor'), 'name: should be preserved for agents');
|
||||
});
|
||||
|
||||
test('adds mode: subagent', () => {
|
||||
const result = convertClaudeToOpencodeFrontmatter(SAMPLE_AGENT, { isAgent: true });
|
||||
const frontmatter = result.split('---')[1];
|
||||
assert.ok(frontmatter.includes('mode: subagent'), 'mode: subagent should be added');
|
||||
});
|
||||
test('does not add model: inherit', () => {
|
||||
const result = convert(SAMPLE_AGENT, { isAgent: true });
|
||||
const frontmatter = result.split('---')[1];
|
||||
assert.ok(!frontmatter.includes('model: inherit'), 'model: inherit should NOT be added');
|
||||
});
|
||||
|
||||
test('strips tools: field', () => {
|
||||
const result = convertClaudeToOpencodeFrontmatter(SAMPLE_AGENT, { isAgent: true });
|
||||
const frontmatter = result.split('---')[1];
|
||||
assert.ok(!frontmatter.includes('tools:'), 'tools: should be stripped for agents');
|
||||
assert.ok(!frontmatter.includes('read: true'), 'tools object should not be generated');
|
||||
});
|
||||
test('adds mode: subagent', () => {
|
||||
const result = convert(SAMPLE_AGENT, { isAgent: true });
|
||||
const frontmatter = result.split('---')[1];
|
||||
assert.ok(frontmatter.includes('mode: subagent'), 'mode: subagent should be added');
|
||||
});
|
||||
|
||||
test('strips skills: array', () => {
|
||||
const result = convertClaudeToOpencodeFrontmatter(SAMPLE_AGENT, { isAgent: true });
|
||||
const frontmatter = result.split('---')[1];
|
||||
assert.ok(!frontmatter.includes('skills:'), 'skills: should be stripped');
|
||||
assert.ok(!frontmatter.includes('gsd-executor-workflow'), 'skill entries should be stripped');
|
||||
});
|
||||
test('strips tools: field', () => {
|
||||
const result = convert(SAMPLE_AGENT, { isAgent: true });
|
||||
const frontmatter = result.split('---')[1];
|
||||
assert.ok(!frontmatter.includes('tools:'), 'tools: should be stripped for agents');
|
||||
assert.ok(!frontmatter.includes('read: true'), 'tools object should not be generated');
|
||||
});
|
||||
|
||||
test('strips color: field', () => {
|
||||
const result = convertClaudeToOpencodeFrontmatter(SAMPLE_AGENT, { isAgent: true });
|
||||
const frontmatter = result.split('---')[1];
|
||||
assert.ok(!frontmatter.includes('color:'), 'color: should be stripped for agents');
|
||||
});
|
||||
test('strips skills: array', () => {
|
||||
const result = convert(SAMPLE_AGENT, { isAgent: true });
|
||||
const frontmatter = result.split('---')[1];
|
||||
assert.ok(!frontmatter.includes('skills:'), 'skills: should be stripped');
|
||||
assert.ok(!frontmatter.includes('gsd-executor-workflow'), 'skill entries should be stripped');
|
||||
});
|
||||
|
||||
test('strips commented hooks block', () => {
|
||||
const result = convertClaudeToOpencodeFrontmatter(SAMPLE_AGENT, { isAgent: true });
|
||||
const frontmatter = result.split('---')[1];
|
||||
assert.ok(!frontmatter.includes('# hooks:'), 'commented hooks should be stripped');
|
||||
assert.ok(!frontmatter.includes('PostToolUse'), 'hook content should be stripped');
|
||||
});
|
||||
test('strips color: field', () => {
|
||||
const result = convert(SAMPLE_AGENT, { isAgent: true });
|
||||
const frontmatter = result.split('---')[1];
|
||||
assert.ok(!frontmatter.includes('color:'), 'color: should be stripped for agents');
|
||||
});
|
||||
|
||||
test('keeps description: field', () => {
|
||||
const result = convertClaudeToOpencodeFrontmatter(SAMPLE_AGENT, { isAgent: true });
|
||||
const frontmatter = result.split('---')[1];
|
||||
assert.ok(frontmatter.includes('description: Executes GSD plans'), 'description should be kept');
|
||||
});
|
||||
test('strips commented hooks block', () => {
|
||||
const result = convert(SAMPLE_AGENT, { isAgent: true });
|
||||
const frontmatter = result.split('---')[1];
|
||||
assert.ok(!frontmatter.includes('# hooks:'), 'commented hooks should be stripped');
|
||||
assert.ok(!frontmatter.includes('PostToolUse'), 'hook content should be stripped');
|
||||
});
|
||||
|
||||
test('preserves body content', () => {
|
||||
const result = convertClaudeToOpencodeFrontmatter(SAMPLE_AGENT, { isAgent: true });
|
||||
assert.ok(result.includes('<role>'), 'body should be preserved');
|
||||
assert.ok(result.includes('You are a GSD plan executor.'), 'body content should be intact');
|
||||
});
|
||||
test('keeps description: field', () => {
|
||||
const result = convert(SAMPLE_AGENT, { isAgent: true });
|
||||
const frontmatter = result.split('---')[1];
|
||||
assert.ok(frontmatter.includes('description: Executes GSD plans'), 'description should be kept');
|
||||
});
|
||||
|
||||
test('applies body text replacements', () => {
|
||||
const agentWithClaudePaths = `---
|
||||
test('preserves body content', () => {
|
||||
const result = convert(SAMPLE_AGENT, { isAgent: true });
|
||||
assert.ok(result.includes('<role>'), 'body should be preserved');
|
||||
assert.ok(result.includes('You are a GSD plan executor.'), 'body content should be intact');
|
||||
});
|
||||
|
||||
test('applies body text replacements', () => {
|
||||
const agentWithClaudePaths = `---
|
||||
name: test-agent
|
||||
description: Test
|
||||
tools: Read
|
||||
@@ -120,32 +135,33 @@ tools: Read
|
||||
Read ~/.claude/agent-memory/ for context.
|
||||
Use $HOME/.claude/skills/ for reference.`;
|
||||
|
||||
const result = convertClaudeToOpencodeFrontmatter(agentWithClaudePaths, { isAgent: true });
|
||||
assert.ok(result.includes('~/.config/opencode/agent-memory/'), '~/.claude should be replaced');
|
||||
assert.ok(result.includes('$HOME/.config/opencode/skills/'), '$HOME/.claude should be replaced');
|
||||
});
|
||||
});
|
||||
|
||||
describe('OpenCode command conversion (isAgent: false, default)', () => {
|
||||
test('strips name: field for commands', () => {
|
||||
const result = convertClaudeToOpencodeFrontmatter(SAMPLE_COMMAND);
|
||||
const frontmatter = result.split('---')[1];
|
||||
assert.ok(!frontmatter.includes('name:'), 'name: should be stripped for commands');
|
||||
const result = convert(agentWithClaudePaths, { isAgent: true });
|
||||
assert.ok(result.includes(`~/${configDir}/agent-memory/`), '~/.claude should be replaced');
|
||||
assert.ok(result.includes(`$HOME/${configDir}/skills/`), '$HOME/.claude should be replaced');
|
||||
});
|
||||
});
|
||||
|
||||
test('does not add model: or mode: for commands', () => {
|
||||
const result = convertClaudeToOpencodeFrontmatter(SAMPLE_COMMAND);
|
||||
const frontmatter = result.split('---')[1];
|
||||
assert.ok(!frontmatter.includes('model:'), 'model: should not be added for commands');
|
||||
assert.ok(!frontmatter.includes('mode:'), 'mode: should not be added for commands');
|
||||
});
|
||||
describe(`${label} command conversion (isAgent: false, default)`, () => {
|
||||
test('strips name: field for commands', () => {
|
||||
const result = convert(SAMPLE_COMMAND);
|
||||
const frontmatter = result.split('---')[1];
|
||||
assert.ok(!frontmatter.includes('name:'), 'name: should be stripped for commands');
|
||||
});
|
||||
|
||||
test('keeps description: for commands', () => {
|
||||
const result = convertClaudeToOpencodeFrontmatter(SAMPLE_COMMAND);
|
||||
const frontmatter = result.split('---')[1];
|
||||
assert.ok(frontmatter.includes('description:'), 'description should be kept');
|
||||
test('does not add model: or mode: for commands', () => {
|
||||
const result = convert(SAMPLE_COMMAND);
|
||||
const frontmatter = result.split('---')[1];
|
||||
assert.ok(!frontmatter.includes('model:'), 'model: should not be added for commands');
|
||||
assert.ok(!frontmatter.includes('mode:'), 'mode: should not be added for commands');
|
||||
});
|
||||
|
||||
test('keeps description: for commands', () => {
|
||||
const result = convert(SAMPLE_COMMAND);
|
||||
const frontmatter = result.split('---')[1];
|
||||
assert.ok(frontmatter.includes('description:'), 'description should be kept');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Gemini CLI agent conversion (merged from gemini-config.test.cjs)
|
||||
|
||||
Reference in New Issue
Block a user