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/
.gbrain/
.gbrain-source
.lingma/
.context/
extension/.auth.json
# 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
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:
```bash
@ -121,6 +121,7 @@ Or target a specific agent with `./setup --host <name>`:
| Kiro | `--host kiro` | `~/.kiro/skills/gstack-*/` |
| Hermes | `--host hermes` | `~/.hermes/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).
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 ~/.kiro/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
rm -f /tmp/gstack-* 2>/dev/null

View File

@ -1,7 +1,7 @@
# Adding a New Host to gstack
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
and re-exporting it. Zero code changes to the generator, setup, or tooling.
@ -17,6 +17,7 @@ hosts/
├── slate.ts # Slate (Random Labs)
├── cursor.ts # Cursor
├── openclaw.ts # OpenClaw (hybrid: config + adapter)
├── qoder.ts # Qoder (通义灵码)
└── index.ts # Registry: imports all, derives Host type
```

View File

@ -16,9 +16,10 @@ import cursor from './cursor';
import openclaw from './openclaw';
import hermes from './hermes';
import gbrain from './gbrain';
import qoder from './qoder';
/** 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. */
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
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"
OPENCODE_SKILLS="$HOME/.config/opencode/skills"
OPENCODE_GSTACK="$OPENCODE_SKILLS/gstack"
QODER_SKILLS="$HOME/.lingma/skills"
QODER_GSTACK="$QODER_SKILLS/gstack"
IS_WINDOWS=0
case "$(uname -s)" in
@ -85,7 +87,7 @@ NO_TEAM_MODE=0
PLAN_TUNE_HOOKS_MODE="" # "" = resolve from env/config/prompt; "yes"/"no" = explicit
while [ $# -gt 0 ]; do
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 ;;
--local) LOCAL_INSTALL=1; shift ;;
--prefix) SKILL_PREFIX=1; SKILL_PREFIX_FLAG=1; shift ;;
@ -101,7 +103,7 @@ while [ $# -gt 0 ]; do
done
case "$HOST" in
claude|codex|kiro|factory|opencode|auto) ;;
claude|codex|kiro|factory|opencode|qoder|auto) ;;
openclaw)
echo ""
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 ""
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
# ─── Resolve skill prefix preference ─────────────────────────
@ -200,14 +202,16 @@ INSTALL_CODEX=0
INSTALL_KIRO=0
INSTALL_FACTORY=0
INSTALL_OPENCODE=0
INSTALL_QODER=0
if [ "$HOST" = "auto" ]; then
command -v claude >/dev/null 2>&1 && INSTALL_CLAUDE=1
command -v codex >/dev/null 2>&1 && INSTALL_CODEX=1
command -v kiro-cli >/dev/null 2>&1 && INSTALL_KIRO=1
command -v droid >/dev/null 2>&1 && INSTALL_FACTORY=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 [ "$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
fi
elif [ "$HOST" = "claude" ]; then
@ -220,6 +224,8 @@ elif [ "$HOST" = "factory" ]; then
INSTALL_FACTORY=1
elif [ "$HOST" = "opencode" ]; then
INSTALL_OPENCODE=1
elif [ "$HOST" = "qoder" ]; then
INSTALL_QODER=1
fi
migrate_direct_codex_install() {
@ -908,6 +914,59 @@ create_opencode_runtime_root() {
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() {
local gstack_dir="$1"
local skills_dir="$2"
@ -972,6 +1031,38 @@ link_opencode_skill_dirs() {
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)
SKILLS_BASENAME="$(basename "$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"
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.
# 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.

View File

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

View File

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