feat: add qoder host support

Add Qoder (通义灵码) as a new gstack host with official .lingma/skills/ paths.

Changes:
- hosts/qoder.ts: new host config with .lingma/ paths
- hosts/index.ts: register qoder in ALL_HOST_CONFIGS
- setup: add qoder install variables, auto-detect, create_qoder_runtime_root(),
  link_qoder_skill_dirs(), and install block
- .gitignore: add .lingma/
- README.md: add qoder to host table, update agent count to 11,
  add qoder cleanup command
- docs/ADDING_A_HOST.md: add qoder to host list
- test/host-config.test.ts: update count to 11, add qoder assertion
- test/gen-skill-docs.test.ts: update setup host validation string
This commit is contained in:
Huang 2026-06-04 04:48:04 +08:00
parent c43c850cae
commit 3274336241
8 changed files with 168 additions and 12 deletions

1
.gitignore vendored
View File

@ -19,6 +19,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.

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,7 @@ 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-*/` |
**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.
@ -358,6 +359,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
``` ```

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;

109
setup
View File

@ -24,6 +24,8 @@ 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
@ -85,7 +87,7 @@ NO_TEAM_MODE=0
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 ;;
--prefix) SKILL_PREFIX=1; SKILL_PREFIX_FLAG=1; shift ;; --prefix) SKILL_PREFIX=1; SKILL_PREFIX_FLAG=1; shift ;;
@ -101,7 +103,7 @@ while [ $# -gt 0 ]; do
done done
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 +138,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 +202,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 +224,8 @@ 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 fi
migrate_direct_codex_install() { migrate_direct_codex_install() {
@ -908,6 +914,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 +1031,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 +1284,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

@ -2343,9 +2343,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', () => {