This commit is contained in:
Andriy 2026-07-14 18:26:06 -07:00 committed by GitHub
commit 0f8ecea6da
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 242 additions and 22 deletions

2
.gitignore vendored
View File

@ -23,6 +23,7 @@ bin/gstack-global-discover*
.hermes/ .hermes/
.gbrain/ .gbrain/
.gbrain-source .gbrain-source
.lingma/
.context/ .context/
extension/.auth.json extension/.auth.json
# xterm assets are vendored from npm at build time; not source-of-truth. # xterm assets are vendored from npm at build time; not source-of-truth.
@ -44,3 +45,4 @@ docs/throughput-*.json
# gbrain local source-staging dir (capability checks, source clones) — runtime artifact # gbrain local source-staging dir (capability checks, source clones) — runtime artifact
.sources/ .sources/
.playwright-browsers

View File

@ -101,7 +101,7 @@ These are conversational skills. Your OpenClaw agent runs them directly via chat
### Other AI Agents ### Other AI Agents
gstack works on 10 AI coding agents, not just Claude. Setup auto-detects which gstack works on 11 AI coding agents, not just Claude. Setup auto-detects which
agents you have installed: agents you have installed:
```bash ```bash
@ -121,6 +121,15 @@ Or target a specific agent with `./setup --host <name>`:
| Kiro | `--host kiro` | `~/.kiro/skills/gstack-*/` | | Kiro | `--host kiro` | `~/.kiro/skills/gstack-*/` |
| Hermes | `--host hermes` | `~/.hermes/skills/gstack-*/` | | Hermes | `--host hermes` | `~/.hermes/skills/gstack-*/` |
| GBrain (mod) | `--host gbrain` | `~/.gbrain/skills/gstack-*/` | | GBrain (mod) | `--host gbrain` | `~/.gbrain/skills/gstack-*/` |
| Qoder | `--host qoder` | `~/.lingma/skills/gstack-*/` |
You can also install skills to a custom directory with `--dir` (useful for
project-level skill installations):
```bash
# Install Qoder skills to a project-local directory
./setup --host qoder --dir ./my-project/.lingma/skills
```
**Want to add support for another agent?** See [docs/ADDING_A_HOST.md](docs/ADDING_A_HOST.md). **Want to add support for another agent?** See [docs/ADDING_A_HOST.md](docs/ADDING_A_HOST.md).
It's one TypeScript config file, zero code changes. It's one TypeScript config file, zero code changes.
@ -360,6 +369,7 @@ rm -rf ~/.codex/skills/gstack* 2>/dev/null
rm -rf ~/.factory/skills/gstack* 2>/dev/null rm -rf ~/.factory/skills/gstack* 2>/dev/null
rm -rf ~/.kiro/skills/gstack* 2>/dev/null rm -rf ~/.kiro/skills/gstack* 2>/dev/null
rm -rf ~/.openclaw/skills/gstack* 2>/dev/null rm -rf ~/.openclaw/skills/gstack* 2>/dev/null
rm -rf ~/.lingma/skills/gstack* 2>/dev/null
# 6. Remove temp files # 6. Remove temp files
rm -f /tmp/gstack-* 2>/dev/null rm -f /tmp/gstack-* 2>/dev/null

View File

@ -1,7 +1,7 @@
# Adding a New Host to gstack # Adding a New Host to gstack
gstack uses a declarative host config system. Each supported AI coding agent gstack uses a declarative host config system. Each supported AI coding agent
(Claude, Codex, Factory, Kiro, OpenCode, Slate, Cursor, OpenClaw) is defined (Claude, Codex, Factory, Kiro, OpenCode, Slate, Cursor, OpenClaw, Qoder) is defined
as a typed TypeScript config object. Adding a new host means creating one file as a typed TypeScript config object. Adding a new host means creating one file
and re-exporting it. Zero code changes to the generator, setup, or tooling. and re-exporting it. Zero code changes to the generator, setup, or tooling.
@ -17,6 +17,7 @@ hosts/
├── slate.ts # Slate (Random Labs) ├── slate.ts # Slate (Random Labs)
├── cursor.ts # Cursor ├── cursor.ts # Cursor
├── openclaw.ts # OpenClaw (hybrid: config + adapter) ├── openclaw.ts # OpenClaw (hybrid: config + adapter)
├── qoder.ts # Qoder (通义灵码)
└── index.ts # Registry: imports all, derives Host type └── index.ts # Registry: imports all, derives Host type
``` ```
@ -141,6 +142,28 @@ freshness check passes, codex skill excluded.
Add install instructions for the new host in the appropriate section. Add install instructions for the new host in the appropriate section.
### 7. (Optional) Support project-level installation with `--dir`
If your host supports installing skills to a project-local directory (e.g. for
team-wide or repo-specific skill installations), extend the setup script to
support the `--dir` flag. Add your host to the `case` statement that handles
`CUSTOM_DIR` overrides in the `setup` script:
```bash
case "$HOST" in
myhost)
MYHOST_SKILLS="$CUSTOM_DIR"
MYHOST_GSTACK="$MYHOST_SKILLS/gstack"
;;
esac
```
Users can then install skills to a custom directory:
```bash
./setup --host myhost --dir ./my-project/.myhost/skills
```
## Config field reference ## Config field reference
See `scripts/host-config.ts` for the full `HostConfig` interface with JSDoc See `scripts/host-config.ts` for the full `HostConfig` interface with JSDoc

View File

@ -16,9 +16,10 @@ import cursor from './cursor';
import openclaw from './openclaw'; import openclaw from './openclaw';
import hermes from './hermes'; import hermes from './hermes';
import gbrain from './gbrain'; import gbrain from './gbrain';
import qoder from './qoder';
/** All registered host configs. Add new hosts here. */ /** All registered host configs. Add new hosts here. */
export const ALL_HOST_CONFIGS: HostConfig[] = [claude, codex, factory, kiro, opencode, slate, cursor, openclaw, hermes, gbrain]; export const ALL_HOST_CONFIGS: HostConfig[] = [claude, codex, factory, kiro, opencode, slate, cursor, openclaw, hermes, gbrain, qoder];
/** Map from host name to config. */ /** Map from host name to config. */
export const HOST_CONFIG_MAP: Record<string, HostConfig> = Object.fromEntries( export const HOST_CONFIG_MAP: Record<string, HostConfig> = Object.fromEntries(
@ -65,4 +66,4 @@ export function getExternalHosts(): HostConfig[] {
} }
// Re-export individual configs for direct import // Re-export individual configs for direct import
export { claude, codex, factory, kiro, opencode, slate, cursor, openclaw, hermes, gbrain }; export { claude, codex, factory, kiro, opencode, slate, cursor, openclaw, hermes, gbrain, qoder };

48
hosts/qoder.ts Normal file
View File

@ -0,0 +1,48 @@
import type { HostConfig } from '../scripts/host-config';
const qoder: HostConfig = {
name: 'qoder',
displayName: 'Qoder',
cliCommand: 'qoder',
cliAliases: [],
globalRoot: '.lingma/skills/gstack',
localSkillRoot: '.lingma/skills/gstack',
hostSubdir: '.lingma',
usesEnvVars: true,
frontmatter: {
mode: 'allowlist',
keepFields: ['name', 'description'],
descriptionLimit: null,
},
generation: {
generateMetadata: false,
skipSkills: ['codex'],
},
pathRewrites: [
{ from: '~/.claude/skills/gstack', to: '~/.lingma/skills/gstack' },
{ from: '.claude/skills/gstack', to: '.lingma/skills/gstack' },
{ from: '.claude/skills', to: '.lingma/skills' },
],
suppressedResolvers: ['GBRAIN_CONTEXT_LOAD', 'GBRAIN_SAVE_RESULTS'],
runtimeRoot: {
globalSymlinks: ['bin', 'browse/dist', 'browse/bin', 'design/dist', 'gstack-upgrade', 'ETHOS.md', 'review/specialists', 'qa/templates', 'qa/references', 'plan-devex-review/dx-hall-of-fame.md'],
globalFiles: {
'review': ['checklist.md', 'design-checklist.md', 'greptile-triage.md', 'TODOS-format.md'],
},
},
install: {
prefixable: false,
linkingStrategy: 'symlink-generated',
},
learningsMode: 'basic',
};
export default qoder;

162
setup
View File

@ -18,12 +18,19 @@ INSTALL_GSTACK_DIR="$(cd "$(dirname "$0")" && pwd)"
SOURCE_GSTACK_DIR="$(cd "$(dirname "$0")" && pwd -P)" SOURCE_GSTACK_DIR="$(cd "$(dirname "$0")" && pwd -P)"
INSTALL_SKILLS_DIR="$(dirname "$INSTALL_GSTACK_DIR")" INSTALL_SKILLS_DIR="$(dirname "$INSTALL_GSTACK_DIR")"
BROWSE_BIN="$SOURCE_GSTACK_DIR/browse/dist/browse" BROWSE_BIN="$SOURCE_GSTACK_DIR/browse/dist/browse"
# Use custom Playwright browsers path if manually installed (avoids macOS cache permission issues)
if [ -d "$SOURCE_GSTACK_DIR/.playwright-browsers" ]; then
export PLAYWRIGHT_BROWSERS_PATH="$SOURCE_GSTACK_DIR/.playwright-browsers"
fi
CODEX_SKILLS="$HOME/.codex/skills" CODEX_SKILLS="$HOME/.codex/skills"
CODEX_GSTACK="$CODEX_SKILLS/gstack" CODEX_GSTACK="$CODEX_SKILLS/gstack"
FACTORY_SKILLS="$HOME/.factory/skills" FACTORY_SKILLS="$HOME/.factory/skills"
FACTORY_GSTACK="$FACTORY_SKILLS/gstack" FACTORY_GSTACK="$FACTORY_SKILLS/gstack"
OPENCODE_SKILLS="$HOME/.config/opencode/skills" OPENCODE_SKILLS="$HOME/.config/opencode/skills"
OPENCODE_GSTACK="$OPENCODE_SKILLS/gstack" OPENCODE_GSTACK="$OPENCODE_SKILLS/gstack"
QODER_SKILLS="$HOME/.lingma/skills"
QODER_GSTACK="$QODER_SKILLS/gstack"
IS_WINDOWS=0 IS_WINDOWS=0
case "$(uname -s)" in case "$(uname -s)" in
@ -82,12 +89,15 @@ SKILL_PREFIX=1
SKILL_PREFIX_FLAG=0 SKILL_PREFIX_FLAG=0
TEAM_MODE=0 TEAM_MODE=0
NO_TEAM_MODE=0 NO_TEAM_MODE=0
CUSTOM_DIR=""
PLAN_TUNE_HOOKS_MODE="" # "" = resolve from env/config/prompt; "yes"/"no" = explicit PLAN_TUNE_HOOKS_MODE="" # "" = resolve from env/config/prompt; "yes"/"no" = explicit
while [ $# -gt 0 ]; do while [ $# -gt 0 ]; do
case "$1" in case "$1" in
--host) [ -z "$2" ] && echo "Missing value for --host (expected claude, codex, kiro, factory, opencode, openclaw, hermes, gbrain, or auto)" >&2 && exit 1; HOST="$2"; shift 2 ;; --host) [ -z "$2" ] && echo "Missing value for --host (expected claude, codex, kiro, factory, opencode, qoder, openclaw, hermes, gbrain, or auto)" >&2 && exit 1; HOST="$2"; shift 2 ;;
--host=*) HOST="${1#--host=}"; shift ;; --host=*) HOST="${1#--host=}"; shift ;;
--local) LOCAL_INSTALL=1; shift ;; --local) LOCAL_INSTALL=1; shift ;;
--dir) [ -z "$2" ] && echo "Missing value for --dir (expected a directory path)" >&2 && exit 1; CUSTOM_DIR="$2"; shift 2 ;;
--dir=*) CUSTOM_DIR="${1#--dir=}"; shift ;;
--prefix) SKILL_PREFIX=1; SKILL_PREFIX_FLAG=1; shift ;; --prefix) SKILL_PREFIX=1; SKILL_PREFIX_FLAG=1; shift ;;
--no-prefix) SKILL_PREFIX=0; SKILL_PREFIX_FLAG=1; shift ;; --no-prefix) SKILL_PREFIX=0; SKILL_PREFIX_FLAG=1; shift ;;
--team) TEAM_MODE=1; shift ;; --team) TEAM_MODE=1; shift ;;
@ -100,8 +110,13 @@ while [ $# -gt 0 ]; do
esac esac
done done
# Expand ~ in --dir argument (bash doesn't expand it inside quoted strings)
if [ -n "$CUSTOM_DIR" ] && [ "${CUSTOM_DIR#\~}" != "$CUSTOM_DIR" ]; then
CUSTOM_DIR="${HOME}${CUSTOM_DIR#\~}"
fi
case "$HOST" in case "$HOST" in
claude|codex|kiro|factory|opencode|auto) ;; claude|codex|kiro|factory|opencode|qoder|auto) ;;
openclaw) openclaw)
echo "" echo ""
echo "OpenClaw integration uses a different model — OpenClaw spawns Claude Code" echo "OpenClaw integration uses a different model — OpenClaw spawns Claude Code"
@ -136,7 +151,7 @@ case "$HOST" in
echo "GBrain setup and brain skills ship from the GBrain repo." echo "GBrain setup and brain skills ship from the GBrain repo."
echo "" echo ""
exit 0 ;; exit 0 ;;
*) echo "Unknown --host value: $HOST (expected claude, codex, kiro, factory, opencode, openclaw, hermes, gbrain, or auto)" >&2; exit 1 ;; *) echo "Unknown --host value: $HOST (expected claude, codex, kiro, factory, opencode, qoder, openclaw, hermes, gbrain, or auto)" >&2; exit 1 ;;
esac esac
# ─── Resolve skill prefix preference ───────────────────────── # ─── Resolve skill prefix preference ─────────────────────────
@ -200,14 +215,16 @@ INSTALL_CODEX=0
INSTALL_KIRO=0 INSTALL_KIRO=0
INSTALL_FACTORY=0 INSTALL_FACTORY=0
INSTALL_OPENCODE=0 INSTALL_OPENCODE=0
INSTALL_QODER=0
if [ "$HOST" = "auto" ]; then if [ "$HOST" = "auto" ]; then
command -v claude >/dev/null 2>&1 && INSTALL_CLAUDE=1 command -v claude >/dev/null 2>&1 && INSTALL_CLAUDE=1
command -v codex >/dev/null 2>&1 && INSTALL_CODEX=1 command -v codex >/dev/null 2>&1 && INSTALL_CODEX=1
command -v kiro-cli >/dev/null 2>&1 && INSTALL_KIRO=1 command -v kiro-cli >/dev/null 2>&1 && INSTALL_KIRO=1
command -v droid >/dev/null 2>&1 && INSTALL_FACTORY=1 command -v droid >/dev/null 2>&1 && INSTALL_FACTORY=1
command -v opencode >/dev/null 2>&1 && INSTALL_OPENCODE=1 command -v opencode >/dev/null 2>&1 && INSTALL_OPENCODE=1
command -v qoder >/dev/null 2>&1 && INSTALL_QODER=1
# If none found, default to claude # If none found, default to claude
if [ "$INSTALL_CLAUDE" -eq 0 ] && [ "$INSTALL_CODEX" -eq 0 ] && [ "$INSTALL_KIRO" -eq 0 ] && [ "$INSTALL_FACTORY" -eq 0 ] && [ "$INSTALL_OPENCODE" -eq 0 ]; then if [ "$INSTALL_CLAUDE" -eq 0 ] && [ "$INSTALL_CODEX" -eq 0 ] && [ "$INSTALL_KIRO" -eq 0 ] && [ "$INSTALL_FACTORY" -eq 0 ] && [ "$INSTALL_OPENCODE" -eq 0 ] && [ "$INSTALL_QODER" -eq 0 ]; then
INSTALL_CLAUDE=1 INSTALL_CLAUDE=1
fi fi
elif [ "$HOST" = "claude" ]; then elif [ "$HOST" = "claude" ]; then
@ -220,6 +237,20 @@ elif [ "$HOST" = "factory" ]; then
INSTALL_FACTORY=1 INSTALL_FACTORY=1
elif [ "$HOST" = "opencode" ]; then elif [ "$HOST" = "opencode" ]; then
INSTALL_OPENCODE=1 INSTALL_OPENCODE=1
elif [ "$HOST" = "qoder" ]; then
INSTALL_QODER=1
fi
# ─── Apply custom directory override ──────────────────────────
# When --dir is specified, override the target skills directory
# for the selected host (supports project-level installations).
if [ -n "$CUSTOM_DIR" ]; then
case "$HOST" in
qoder)
QODER_SKILLS="$CUSTOM_DIR"
QODER_GSTACK="$QODER_SKILLS/gstack"
;;
esac
fi fi
migrate_direct_codex_install() { migrate_direct_codex_install() {
@ -255,12 +286,12 @@ ensure_playwright_browser() {
# (oven-sh/bun#4253). Use Node.js to verify Chromium works instead. # (oven-sh/bun#4253). Use Node.js to verify Chromium works instead.
( (
cd "$SOURCE_GSTACK_DIR" cd "$SOURCE_GSTACK_DIR"
node -e "const { chromium } = require('playwright'); (async () => { const b = await chromium.launch(); await b.close(); })()" 2>/dev/null node -e "const { chromium } = require('playwright'); (async () => { const b = await chromium.launch({ channel: 'chromium' }); await b.close(); })()" 2>/dev/null
) )
else else
( (
cd "$SOURCE_GSTACK_DIR" cd "$SOURCE_GSTACK_DIR"
bun --eval 'import { chromium } from "playwright"; const browser = await chromium.launch(); await browser.close();' bun --eval 'import { chromium } from "playwright"; const browser = await chromium.launch({ channel: "chromium" }); await browser.close();'
) >/dev/null 2>&1 ) >/dev/null 2>&1
fi fi
} }
@ -477,11 +508,17 @@ fi
# 2. Ensure Playwright's Chromium is available # 2. Ensure Playwright's Chromium is available
if ! ensure_playwright_browser; then if ! ensure_playwright_browser; then
echo "Installing Playwright Chromium..." # Skip download if browser already exists in custom path (avoids macOS cache hang)
( if [ -n "${PLAYWRIGHT_BROWSERS_PATH:-}" ] && [ -d "$PLAYWRIGHT_BROWSERS_PATH/chromium-1208" ]; then
cd "$SOURCE_GSTACK_DIR" echo "Playwright Chromium already installed at $PLAYWRIGHT_BROWSERS_PATH"
bunx playwright install chromium else
) echo "Installing Playwright Chromium..."
(
cd "$SOURCE_GSTACK_DIR"
# Use timeout to avoid hanging on macOS cache issues (timeout returns 124 on macOS)
timeout 120 bunx playwright install chromium 2>/dev/null || echo " Playwright install timed out or failed — browser-dependent skills may not work"
)
fi
if [ "$IS_WINDOWS" -eq 1 ]; then if [ "$IS_WINDOWS" -eq 1 ]; then
# On Windows, Node.js launches Chromium (not Bun — see oven-sh/bun#4253). # On Windows, Node.js launches Chromium (not Bun — see oven-sh/bun#4253).
@ -506,13 +543,15 @@ fi
if ! ensure_playwright_browser; then if ! ensure_playwright_browser; then
if [ "$IS_WINDOWS" -eq 1 ]; then if [ "$IS_WINDOWS" -eq 1 ]; then
echo "gstack setup failed: Playwright Chromium could not be launched via Node.js" >&2 echo "Warning: Playwright Chromium could not be launched via Node.js" >&2
echo " This is a known issue with Bun on Windows (oven-sh/bun#4253)." >&2 echo " This is a known issue with Bun on Windows (oven-sh/bun#4253)." >&2
echo " Ensure Node.js is installed and 'node -e \"require('playwright')\"' works." >&2 echo " Ensure Node.js is installed and 'node -e \"require('playwright')\"' works." >&2
echo " Browser-dependent skills (/browse, /qa) will not work until fixed." >&2
else else
echo "gstack setup failed: Playwright Chromium could not be launched" >&2 echo "Warning: Playwright Chromium could not be launched" >&2
echo " Browser-dependent skills (/browse, /qa) will not work until fixed." >&2
echo " To fix: 'bunx playwright install chromium' or set GSTACK_SKIP_BROWSER_CHECK=1" >&2
fi fi
exit 1
fi fi
# 2b. Ensure a color-emoji font is installed so make-pdf emoji render (Linux). # 2b. Ensure a color-emoji font is installed so make-pdf emoji render (Linux).
@ -908,6 +947,59 @@ create_opencode_runtime_root() {
fi fi
} }
create_qoder_runtime_root() {
local gstack_dir="$1"
local qoder_gstack="$2"
local qoder_dir="$gstack_dir/.lingma/skills"
if [ -L "$qoder_gstack" ]; then
rm -f "$qoder_gstack"
elif [ -d "$qoder_gstack" ] && [ "$qoder_gstack" != "$gstack_dir" ]; then
rm -rf "$qoder_gstack"
fi
mkdir -p "$qoder_gstack" "$qoder_gstack/browse" "$qoder_gstack/design" "$qoder_gstack/gstack-upgrade" "$qoder_gstack/review" "$qoder_gstack/qa" "$qoder_gstack/plan-devex-review"
if [ -f "$qoder_dir/gstack/SKILL.md" ]; then
_link_or_copy "$qoder_dir/gstack/SKILL.md" "$qoder_gstack/SKILL.md"
fi
if [ -d "$gstack_dir/bin" ]; then
_link_or_copy "$gstack_dir/bin" "$qoder_gstack/bin"
fi
if [ -d "$gstack_dir/browse/dist" ]; then
_link_or_copy "$gstack_dir/browse/dist" "$qoder_gstack/browse/dist"
fi
if [ -d "$gstack_dir/browse/bin" ]; then
_link_or_copy "$gstack_dir/browse/bin" "$qoder_gstack/browse/bin"
fi
if [ -d "$gstack_dir/design/dist" ]; then
_link_or_copy "$gstack_dir/design/dist" "$qoder_gstack/design/dist"
fi
if [ -f "$qoder_dir/gstack-upgrade/SKILL.md" ]; then
_link_or_copy "$qoder_dir/gstack-upgrade/SKILL.md" "$qoder_gstack/gstack-upgrade/SKILL.md"
fi
for f in checklist.md design-checklist.md greptile-triage.md TODOS-format.md; do
if [ -f "$gstack_dir/review/$f" ]; then
_link_or_copy "$gstack_dir/review/$f" "$qoder_gstack/review/$f"
fi
done
if [ -d "$gstack_dir/review/specialists" ]; then
_link_or_copy "$gstack_dir/review/specialists" "$qoder_gstack/review/specialists"
fi
if [ -d "$gstack_dir/qa/templates" ]; then
_link_or_copy "$gstack_dir/qa/templates" "$qoder_gstack/qa/templates"
fi
if [ -d "$gstack_dir/qa/references" ]; then
_link_or_copy "$gstack_dir/qa/references" "$qoder_gstack/qa/references"
fi
if [ -f "$gstack_dir/plan-devex-review/dx-hall-of-fame.md" ]; then
_link_or_copy "$gstack_dir/plan-devex-review/dx-hall-of-fame.md" "$qoder_gstack/plan-devex-review/dx-hall-of-fame.md"
fi
if [ -f "$gstack_dir/ETHOS.md" ]; then
_link_or_copy "$gstack_dir/ETHOS.md" "$qoder_gstack/ETHOS.md"
fi
}
link_factory_skill_dirs() { link_factory_skill_dirs() {
local gstack_dir="$1" local gstack_dir="$1"
local skills_dir="$2" local skills_dir="$2"
@ -972,6 +1064,38 @@ link_opencode_skill_dirs() {
fi fi
} }
link_qoder_skill_dirs() {
local gstack_dir="$1"
local skills_dir="$2"
local qoder_dir="$gstack_dir/.lingma/skills"
local linked=()
if [ ! -d "$qoder_dir" ]; then
echo " Generating .lingma/ skill docs..."
( cd "$gstack_dir" && bun run gen:skill-docs --host qoder )
fi
if [ ! -d "$qoder_dir" ]; then
echo " warning: .lingma/skills/ generation failed — run 'bun run gen:skill-docs --host qoder' manually" >&2
return 1
fi
for skill_dir in "$qoder_dir"/gstack*/; do
if [ -f "$skill_dir/SKILL.md" ]; then
skill_name="$(basename "$skill_dir")"
[ "$skill_name" = "gstack" ] && continue
target="$skills_dir/$skill_name"
if [ -L "$target" ] || [ ! -e "$target" ]; then
_link_or_copy "$skill_dir" "$target"
linked+=("$skill_name")
fi
fi
done
if [ ${#linked[@]} -gt 0 ]; then
echo " linked skills: ${linked[*]}"
fi
}
# 4. Install for Claude (default) # 4. Install for Claude (default)
SKILLS_BASENAME="$(basename "$INSTALL_SKILLS_DIR")" SKILLS_BASENAME="$(basename "$INSTALL_SKILLS_DIR")"
SKILLS_PARENT_BASENAME="$(basename "$(dirname "$INSTALL_SKILLS_DIR")")" SKILLS_PARENT_BASENAME="$(basename "$(dirname "$INSTALL_SKILLS_DIR")")"
@ -1193,6 +1317,16 @@ if [ "$INSTALL_OPENCODE" -eq 1 ]; then
echo " opencode skills: $OPENCODE_SKILLS" echo " opencode skills: $OPENCODE_SKILLS"
fi fi
# 6d. Install for Qoder
if [ "$INSTALL_QODER" -eq 1 ]; then
mkdir -p "$QODER_SKILLS"
create_qoder_runtime_root "$SOURCE_GSTACK_DIR" "$QODER_GSTACK"
link_qoder_skill_dirs "$SOURCE_GSTACK_DIR" "$QODER_SKILLS"
echo "gstack ready (qoder)."
echo " browse: $BROWSE_BIN"
echo " qoder skills: $QODER_SKILLS"
fi
# 7. Create .agents/ sidecar symlinks for the real Codex skill target. # 7. Create .agents/ sidecar symlinks for the real Codex skill target.
# The root Codex skill ends up pointing at $SOURCE_GSTACK_DIR/.agents/skills/gstack, # The root Codex skill ends up pointing at $SOURCE_GSTACK_DIR/.agents/skills/gstack,
# so the runtime assets must live there for both global and repo-local installs. # so the runtime assets must live there for both global and repo-local installs.

View File

@ -2372,9 +2372,9 @@ describe('setup script validation', () => {
expect(claudeSection).toContain('link_claude_root_skill_alias "$SOURCE_GSTACK_DIR" "$INSTALL_SKILLS_DIR"'); expect(claudeSection).toContain('link_claude_root_skill_alias "$SOURCE_GSTACK_DIR" "$INSTALL_SKILLS_DIR"');
}); });
test('setup supports --host auto|claude|codex|kiro|opencode', () => { test('setup supports --host auto|claude|codex|kiro|factory|opencode|qoder', () => {
expect(setupContent).toContain('--host'); expect(setupContent).toContain('--host');
expect(setupContent).toContain('claude|codex|kiro|factory|opencode|auto'); expect(setupContent).toContain('claude|codex|kiro|factory|opencode|qoder|auto');
}); });
test('auto mode detects claude, codex, kiro, and opencode binaries', () => { test('auto mode detects claude, codex, kiro, and opencode binaries', () => {

View File

@ -22,6 +22,7 @@ import {
slate, slate,
cursor, cursor,
openclaw, openclaw,
qoder,
} from '../hosts/index'; } from '../hosts/index';
import { HOST_PATHS } from '../scripts/resolvers/types'; import { HOST_PATHS } from '../scripts/resolvers/types';
@ -30,8 +31,8 @@ const ROOT = path.resolve(import.meta.dir, '..');
// ─── hosts/index.ts ───────────────────────────────────────── // ─── hosts/index.ts ─────────────────────────────────────────
describe('hosts/index.ts', () => { describe('hosts/index.ts', () => {
test('ALL_HOST_CONFIGS has 10 hosts', () => { test('ALL_HOST_CONFIGS has 11 hosts', () => {
expect(ALL_HOST_CONFIGS.length).toBe(10); expect(ALL_HOST_CONFIGS.length).toBe(11);
}); });
test('ALL_HOST_NAMES matches config names', () => { test('ALL_HOST_NAMES matches config names', () => {
@ -53,6 +54,7 @@ describe('hosts/index.ts', () => {
expect(slate.name).toBe('slate'); expect(slate.name).toBe('slate');
expect(cursor.name).toBe('cursor'); expect(cursor.name).toBe('cursor');
expect(openclaw.name).toBe('openclaw'); expect(openclaw.name).toBe('openclaw');
expect(qoder.name).toBe('qoder');
}); });
test('getHostConfig returns correct config', () => { test('getHostConfig returns correct config', () => {