Skip to content

Commit 60c724f

Browse files
feat(hosts): add GitHub Copilot CLI as a first-class host (--host copilot)
Adds support for installing gstack as flat .agent.md files under ~/.copilot/agents/, per Copilot CLI's custom agents schema: https://docs.github.com/en/copilot/reference/custom-agents-configuration The change is purely additive — existing hosts are untouched: - New `outputLayout?: 'per-skill-dir' | 'flat-agent-md'` field on HostConfig. Default is 'per-skill-dir' (every existing host). Copilot uses 'flat-agent-md' to emit one file per skill, flat under <hostSubdir>/agents/. - processExternalHost branches on the new field to write to <hostSubdir>/agents/<name>.agent.md instead of the default <hostSubdir>/skills/<name>/SKILL.md. - hosts/copilot.ts: frontmatter allowlist (name/description), injects target=github-copilot + tools=["*"] per the schema. skipSkills includes 'codex' (per the every-external-host convention) and 'copilot' (to avoid recursing if a /copilot skill is ever added to the source tree). - setup: INSTALL_COPILOT flag, build trigger, install block that symlinks generated gstack-*.agent.md into ~/.copilot/agents/ and creates ~/.copilot/gstack/ runtime root with bin/browse-dist/upgrade symlinks. - Parameterized host smoke tests updated to handle both layouts; --host all test asserts the right output subdir per host. Two assertions updated for the new host count (10 → 11) and the new --host pass-through list. Verified end-to-end on macOS: - `./setup --host copilot` produces 46 agents under ~/.copilot/agents/. - `copilot --agent <name>` lists every gstack skill (autoplan, qa, review, codex, design-shotgun, …). - `bun test test/host-config.test.ts test/gen-skill-docs.test.ts` → 459 pass, 0 fail. Use: `copilot --agent qa "test the staging site"`. First-time setup: `export GSTACK_ROOT=$HOME/.copilot/gstack`.
1 parent 7489506 commit 60c724f

8 files changed

Lines changed: 203 additions & 32 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ bin/gstack-global-discover
1818
.openclaw/
1919
.hermes/
2020
.gbrain/
21+
.copilot/
2122
.gbrain-source
2223
.context/
2324
extension/.auth.json

hosts/copilot.ts

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import type { HostConfig } from '../scripts/host-config';
2+
3+
/**
4+
* GitHub Copilot CLI host.
5+
*
6+
* Copilot CLI discovers custom agents as flat `.agent.md` files in
7+
* `~/.copilot/agents/`. Each file has YAML frontmatter (name, description,
8+
* tools, target, etc.) followed by markdown instructions. Invoke with
9+
* `copilot --agent <name>`.
10+
*
11+
* Schema reference:
12+
* https://docs.github.com/en/copilot/reference/custom-agents-configuration
13+
*
14+
* gstack skills are emitted as `gstack-<skill>.agent.md` (flat with prefix —
15+
* Copilot CLI does not recurse into subdirectories under ~/.copilot/agents/).
16+
*/
17+
const copilot: HostConfig = {
18+
name: 'copilot',
19+
displayName: 'GitHub Copilot CLI',
20+
cliCommand: 'copilot',
21+
22+
globalRoot: '.copilot/agents',
23+
localSkillRoot: '.copilot/agents',
24+
hostSubdir: '.copilot',
25+
usesEnvVars: true,
26+
27+
outputLayout: 'flat-agent-md',
28+
29+
frontmatter: {
30+
mode: 'allowlist',
31+
keepFields: ['name', 'description'],
32+
descriptionLimit: 1024,
33+
descriptionLimitBehavior: 'truncate',
34+
extraFields: {
35+
// `target` is intentionally omitted — defaults to "both" (Copilot CLI + VS Code
36+
// Copilot extension), maximising reach. Set to "github-copilot" or "vscode" to
37+
// narrow if a deployment ever needs that.
38+
// gstack skills need broad tool access. Emit as YAML array via stringified literal —
39+
// transformFrontmatter does string-interpolation, so the value is rendered verbatim.
40+
// (If transformFrontmatter ever gains real array support, switch to a JS array.)
41+
tools: '["*"]',
42+
},
43+
},
44+
45+
generation: {
46+
generateMetadata: false,
47+
// 'codex' — every external host skips codex (it wraps the `codex` CLI binary).
48+
// 'copilot' — the gstack /copilot skill (if present in source) itself shells
49+
// out to the `copilot` CLI binary; exposing it as a copilot agent would
50+
// let Copilot recurse on itself.
51+
skipSkills: ['codex', 'copilot'],
52+
},
53+
54+
pathRewrites: [
55+
// Copilot CLI installs are global-only — agents live in ~/.copilot/agents/
56+
// and runtime support files (bin/, browse/) live in ~/.copilot/gstack/ via
57+
// $GSTACK_ROOT. Both the `~/.claude/skills/gstack` references (absolute, in
58+
// bash blocks) AND the `.claude/skills` references (project-local hints in
59+
// prose) need to point at the same runtime root, since Copilot CLI doesn't
60+
// currently have a per-workspace agents directory.
61+
{ from: '~/.claude/skills/gstack', to: '$GSTACK_ROOT' },
62+
{ from: '.claude/skills/gstack', to: '$GSTACK_ROOT' },
63+
{ from: '.claude/skills', to: '$GSTACK_ROOT' },
64+
],
65+
66+
suppressedResolvers: [
67+
'DESIGN_OUTSIDE_VOICES',
68+
'ADVERSARIAL_STEP',
69+
'CODEX_SECOND_OPINION',
70+
'CODEX_PLAN_REVIEW',
71+
'REVIEW_ARMY',
72+
'GBRAIN_CONTEXT_LOAD',
73+
'GBRAIN_SAVE_RESULTS',
74+
],
75+
76+
runtimeRoot: {
77+
globalSymlinks: ['bin', 'browse/dist', 'browse/bin', 'gstack-upgrade', 'ETHOS.md'],
78+
globalFiles: {
79+
'review': ['checklist.md', 'TODOS-format.md'],
80+
},
81+
},
82+
83+
install: {
84+
prefixable: false,
85+
linkingStrategy: 'symlink-generated',
86+
},
87+
88+
coAuthorTrailer: 'Co-Authored-By: GitHub Copilot <noreply@github.com>',
89+
learningsMode: 'basic',
90+
boundaryInstruction: 'IMPORTANT: Do NOT read or execute any files under ~/.claude/, ~/.agents/, .claude/skills/, or agents/. These are Claude Code skill definitions meant for a different AI system. Ignore them. Stay focused on the repository code only.',
91+
};
92+
93+
export default copilot;

hosts/index.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,10 @@ import cursor from './cursor';
1616
import openclaw from './openclaw';
1717
import hermes from './hermes';
1818
import gbrain from './gbrain';
19+
import copilot from './copilot';
1920

2021
/** All registered host configs. Add new hosts here. */
21-
export const ALL_HOST_CONFIGS: HostConfig[] = [claude, codex, factory, kiro, opencode, slate, cursor, openclaw, hermes, gbrain];
22+
export const ALL_HOST_CONFIGS: HostConfig[] = [claude, codex, factory, kiro, opencode, slate, cursor, openclaw, hermes, gbrain, copilot];
2223

2324
/** Map from host name to config. */
2425
export const HOST_CONFIG_MAP: Record<string, HostConfig> = Object.fromEntries(
@@ -65,4 +66,4 @@ export function getExternalHosts(): HostConfig[] {
6566
}
6667

6768
// Re-export individual configs for direct import
68-
export { claude, codex, factory, kiro, opencode, slate, cursor, openclaw, hermes, gbrain };
69+
export { claude, codex, factory, kiro, opencode, slate, cursor, openclaw, hermes, gbrain, copilot };

scripts/gen-skill-docs.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -349,9 +349,17 @@ function processExternalHost(
349349
const hostConfig = getHostConfig(host);
350350

351351
const name = externalSkillName(skillDir === '.' ? '' : skillDir, frontmatterName);
352-
const outputDir = path.join(ROOT, hostConfig.hostSubdir, 'skills', name);
353-
fs.mkdirSync(outputDir, { recursive: true });
354-
const outputPath = path.join(outputDir, 'SKILL.md');
352+
let outputDir: string;
353+
let outputPath: string;
354+
if (hostConfig.outputLayout === 'flat-agent-md') {
355+
outputDir = path.join(ROOT, hostConfig.hostSubdir, 'agents');
356+
fs.mkdirSync(outputDir, { recursive: true });
357+
outputPath = path.join(outputDir, `${name}.agent.md`);
358+
} else {
359+
outputDir = path.join(ROOT, hostConfig.hostSubdir, 'skills', name);
360+
fs.mkdirSync(outputDir, { recursive: true });
361+
outputPath = path.join(outputDir, 'SKILL.md');
362+
}
355363

356364
// Guard against symlink loops
357365
let symlinkLoop = false;

scripts/host-config.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,15 @@ export interface HostConfig {
6666
includeSkills?: string[];
6767
};
6868

69+
/**
70+
* Output layout for generated skill docs.
71+
* - 'per-skill-dir' (default): <hostSubdir>/skills/<name>/SKILL.md
72+
* Used by Codex, Cursor, Factory, OpenCode, etc.
73+
* - 'flat-agent-md': <hostSubdir>/agents/<name>.agent.md
74+
* Used by GitHub Copilot CLI (~/.copilot/agents/<name>.agent.md).
75+
*/
76+
outputLayout?: 'per-skill-dir' | 'flat-agent-md';
77+
6978
// --- Content Rewrites ---
7079
/** Literal string replacements on generated SKILL.md content. Order matters, replaceAll. */
7180
pathRewrites: Array<{ from: string; to: string }>;

setup

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ FACTORY_SKILLS="$HOME/.factory/skills"
2424
FACTORY_GSTACK="$FACTORY_SKILLS/gstack"
2525
OPENCODE_SKILLS="$HOME/.config/opencode/skills"
2626
OPENCODE_GSTACK="$OPENCODE_SKILLS/gstack"
27+
COPILOT_AGENTS="$HOME/.copilot/agents"
28+
COPILOT_GSTACK="$HOME/.copilot/gstack"
2729

2830
IS_WINDOWS=0
2931
case "$(uname -s)" in
@@ -43,7 +45,7 @@ TEAM_MODE=0
4345
NO_TEAM_MODE=0
4446
while [ $# -gt 0 ]; do
4547
case "$1" in
46-
--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 ;;
48+
--host) [ -z "$2" ] && echo "Missing value for --host (expected claude, codex, kiro, factory, opencode, copilot, openclaw, hermes, gbrain, or auto)" >&2 && exit 1; HOST="$2"; shift 2 ;;
4749
--host=*) HOST="${1#--host=}"; shift ;;
4850
--local) LOCAL_INSTALL=1; shift ;;
4951
--prefix) SKILL_PREFIX=1; SKILL_PREFIX_FLAG=1; shift ;;
@@ -56,7 +58,7 @@ while [ $# -gt 0 ]; do
5658
done
5759

5860
case "$HOST" in
59-
claude|codex|kiro|factory|opencode|auto) ;;
61+
claude|codex|kiro|factory|opencode|copilot|auto) ;;
6062
openclaw)
6163
echo ""
6264
echo "OpenClaw integration uses a different model — OpenClaw spawns Claude Code"
@@ -91,7 +93,7 @@ case "$HOST" in
9193
echo "GBrain setup and brain skills ship from the GBrain repo."
9294
echo ""
9395
exit 0 ;;
94-
*) echo "Unknown --host value: $HOST (expected claude, codex, kiro, factory, opencode, openclaw, hermes, gbrain, or auto)" >&2; exit 1 ;;
96+
*) echo "Unknown --host value: $HOST (expected claude, codex, kiro, factory, opencode, copilot, openclaw, hermes, gbrain, or auto)" >&2; exit 1 ;;
9597
esac
9698

9799
# ─── Resolve skill prefix preference ─────────────────────────
@@ -155,14 +157,16 @@ INSTALL_CODEX=0
155157
INSTALL_KIRO=0
156158
INSTALL_FACTORY=0
157159
INSTALL_OPENCODE=0
160+
INSTALL_COPILOT=0
158161
if [ "$HOST" = "auto" ]; then
159162
command -v claude >/dev/null 2>&1 && INSTALL_CLAUDE=1
160163
command -v codex >/dev/null 2>&1 && INSTALL_CODEX=1
161164
command -v kiro-cli >/dev/null 2>&1 && INSTALL_KIRO=1
162165
command -v droid >/dev/null 2>&1 && INSTALL_FACTORY=1
163166
command -v opencode >/dev/null 2>&1 && INSTALL_OPENCODE=1
167+
command -v copilot >/dev/null 2>&1 && INSTALL_COPILOT=1
164168
# If none found, default to claude
165-
if [ "$INSTALL_CLAUDE" -eq 0 ] && [ "$INSTALL_CODEX" -eq 0 ] && [ "$INSTALL_KIRO" -eq 0 ] && [ "$INSTALL_FACTORY" -eq 0 ] && [ "$INSTALL_OPENCODE" -eq 0 ]; then
169+
if [ "$INSTALL_CLAUDE" -eq 0 ] && [ "$INSTALL_CODEX" -eq 0 ] && [ "$INSTALL_KIRO" -eq 0 ] && [ "$INSTALL_FACTORY" -eq 0 ] && [ "$INSTALL_OPENCODE" -eq 0 ] && [ "$INSTALL_COPILOT" -eq 0 ]; then
166170
INSTALL_CLAUDE=1
167171
fi
168172
elif [ "$HOST" = "claude" ]; then
@@ -175,6 +179,8 @@ elif [ "$HOST" = "factory" ]; then
175179
INSTALL_FACTORY=1
176180
elif [ "$HOST" = "opencode" ]; then
177181
INSTALL_OPENCODE=1
182+
elif [ "$HOST" = "copilot" ]; then
183+
INSTALL_COPILOT=1
178184
fi
179185

180186
migrate_direct_codex_install() {
@@ -321,6 +327,16 @@ if [ "$INSTALL_OPENCODE" -eq 1 ] && [ "$NEEDS_BUILD" -eq 0 ]; then
321327
)
322328
fi
323329

330+
# 1e. Generate .copilot/ GitHub Copilot CLI agent files
331+
if [ "$INSTALL_COPILOT" -eq 1 ] && [ "$NEEDS_BUILD" -eq 0 ]; then
332+
log "Generating .copilot/ agent files..."
333+
(
334+
cd "$SOURCE_GSTACK_DIR"
335+
bun install --frozen-lockfile 2>/dev/null || bun install
336+
bun run gen:skill-docs --host copilot
337+
)
338+
fi
339+
324340
# 2. Ensure Playwright's Chromium is available
325341
if ! ensure_playwright_browser; then
326342
echo "Installing Playwright Chromium..."
@@ -935,6 +951,40 @@ if [ "$INSTALL_OPENCODE" -eq 1 ]; then
935951
echo " opencode skills: $OPENCODE_SKILLS"
936952
fi
937953

954+
# 6d. Install for GitHub Copilot CLI
955+
# Copilot discovers agents as flat `.agent.md` files under ~/.copilot/agents/.
956+
# We symlink each generated `gstack-<skill>.agent.md` from the repo's
957+
# .copilot/agents/ dir, and create ~/.copilot/gstack/ with bin/browse symlinks
958+
# so agents can resolve $GSTACK_ROOT-relative tooling.
959+
if [ "$INSTALL_COPILOT" -eq 1 ]; then
960+
mkdir -p "$COPILOT_AGENTS"
961+
mkdir -p "$COPILOT_GSTACK"
962+
# Clean stale gstack symlinks first so renamed/removed skills don't leave
963+
# orphan entries pointing at non-existent source files.
964+
find "$COPILOT_AGENTS" -maxdepth 1 -name "gstack-*.agent.md" -type l -delete 2>/dev/null || true
965+
# Symlink each agent file (flat layout — Copilot CLI does not recurse)
966+
for f in "$SOURCE_GSTACK_DIR"/.copilot/agents/*.agent.md; do
967+
[ -e "$f" ] || continue # tolerate no matches
968+
ln -sf "$f" "$COPILOT_AGENTS/$(basename "$f")"
969+
done
970+
# Runtime support files referenced by agent prompts via $GSTACK_ROOT
971+
ln -sfn "$SOURCE_GSTACK_DIR/bin" "$COPILOT_GSTACK/bin"
972+
ln -sfn "$SOURCE_GSTACK_DIR/browse/dist" "$COPILOT_GSTACK/browse-dist"
973+
ln -sfn "$SOURCE_GSTACK_DIR/gstack-upgrade" "$COPILOT_GSTACK/gstack-upgrade"
974+
[ -f "$SOURCE_GSTACK_DIR/ETHOS.md" ] && ln -sfn "$SOURCE_GSTACK_DIR/ETHOS.md" "$COPILOT_GSTACK/ETHOS.md"
975+
AGENT_COUNT=$(ls "$COPILOT_AGENTS"/gstack-*.agent.md 2>/dev/null | wc -l | tr -d ' ')
976+
echo "gstack ready (copilot)."
977+
echo " browse: $BROWSE_BIN"
978+
echo " copilot agents: $COPILOT_AGENTS ($AGENT_COUNT gstack-*.agent.md files)"
979+
echo " runtime root: $COPILOT_GSTACK"
980+
echo ""
981+
echo " IMPORTANT: agents reference \$GSTACK_ROOT — set it before launching copilot:"
982+
echo " export GSTACK_ROOT=$COPILOT_GSTACK"
983+
echo " Or persist in ~/.copilot/settings.json under secret-env-vars."
984+
echo ""
985+
echo " Invoke any gstack agent: copilot --agent gstack-<skill> \"<prompt>\""
986+
fi
987+
938988
# 7. Create .agents/ sidecar symlinks for the real Codex skill target.
939989
# The root Codex skill ends up pointing at $SOURCE_GSTACK_DIR/.agents/skills/gstack,
940990
# so the runtime assets must live there for both global and repo-local installs.

test/gen-skill-docs.test.ts

Lines changed: 30 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2069,7 +2069,25 @@ import { ALL_HOST_CONFIGS, getExternalHosts } from '../hosts/index';
20692069
describe('Parameterized host smoke tests', () => {
20702070
for (const hostConfig of getExternalHosts()) {
20712071
describe(`${hostConfig.displayName} (--host ${hostConfig.name})`, () => {
2072-
const hostDir = path.join(ROOT, hostConfig.hostSubdir, 'skills');
2072+
// Layout-aware helpers — `flat-agent-md` hosts (Copilot CLI) emit
2073+
// <hostSubdir>/agents/<name>.agent.md; default hosts emit
2074+
// <hostSubdir>/skills/<name>/SKILL.md.
2075+
const isFlat = hostConfig.outputLayout === 'flat-agent-md';
2076+
const hostDir = path.join(ROOT, hostConfig.hostSubdir, isFlat ? 'agents' : 'skills');
2077+
const skillFilePath = (name: string) => isFlat
2078+
? path.join(hostDir, `${name}.agent.md`)
2079+
: path.join(hostDir, name, 'SKILL.md');
2080+
const listSkillFiles = (): Array<{ name: string; path: string }> => {
2081+
if (!fs.existsSync(hostDir)) return [];
2082+
if (isFlat) {
2083+
return fs.readdirSync(hostDir)
2084+
.filter(f => f.endsWith('.agent.md'))
2085+
.map(f => ({ name: f.replace(/\.agent\.md$/, ''), path: path.join(hostDir, f) }));
2086+
}
2087+
return fs.readdirSync(hostDir)
2088+
.filter(d => fs.existsSync(path.join(hostDir, d, 'SKILL.md')))
2089+
.map(d => ({ name: d, path: path.join(hostDir, d, 'SKILL.md') }));
2090+
};
20732091

20742092
test('generates output that exists on disk', () => {
20752093
// Generated dir should exist (created by earlier bun run gen:skill-docs --host all)
@@ -2080,37 +2098,27 @@ describe('Parameterized host smoke tests', () => {
20802098
});
20812099
}
20822100
expect(fs.existsSync(hostDir)).toBe(true);
2083-
const skills = fs.readdirSync(hostDir).filter(d =>
2084-
fs.existsSync(path.join(hostDir, d, 'SKILL.md'))
2085-
);
2086-
expect(skills.length).toBeGreaterThan(0);
2101+
expect(listSkillFiles().length).toBeGreaterThan(0);
20872102
});
20882103

20892104
test('no .claude/skills path leakage outside repo-root sidecar symlinks', () => {
20902105
if (!fs.existsSync(hostDir)) return; // skip if not generated
2091-
const skills = fs.readdirSync(hostDir);
2092-
for (const skill of skills) {
2106+
for (const { name, path: skillMd } of listSkillFiles()) {
20932107
// Dev installs may mount the repo root at host/skills/gstack as a runtime
20942108
// sidecar. The generator skips that symlink loop, so leakage checks should too.
2095-
if (isRepoRootSymlink(path.join(hostDir, skill))) continue;
2096-
const skillMd = path.join(hostDir, skill, 'SKILL.md');
2097-
if (!fs.existsSync(skillMd)) continue;
2109+
if (!isFlat && isRepoRootSymlink(path.join(hostDir, name))) continue;
20982110
const content = fs.readFileSync(skillMd, 'utf-8');
20992111
// Strip bash blocks (which have legitimate fallback paths)
21002112
const noBash = content.replace(/```bash\n[\s\S]*?```/g, '');
21012113
const leaks = noBash.split('\n').filter(l => l.includes('.claude/skills'));
21022114
if (leaks.length > 0) {
2103-
throw new Error(`${skill}: .claude/skills leakage:\n${leaks.slice(0, 3).join('\n')}`);
2115+
throw new Error(`${name}: .claude/skills leakage:\n${leaks.slice(0, 3).join('\n')}`);
21042116
}
21052117
}
21062118
});
21072119

21082120
test('frontmatter has name and description', () => {
2109-
if (!fs.existsSync(hostDir)) return;
2110-
const skills = fs.readdirSync(hostDir);
2111-
for (const skill of skills) {
2112-
const skillMd = path.join(hostDir, skill, 'SKILL.md');
2113-
if (!fs.existsSync(skillMd)) continue;
2121+
for (const { path: skillMd } of listSkillFiles()) {
21142122
const content = fs.readFileSync(skillMd, 'utf-8');
21152123
expect(content).toMatch(/^---\n/);
21162124
expect(content).toMatch(/^name:\s/m);
@@ -2119,7 +2127,7 @@ describe('Parameterized host smoke tests', () => {
21192127
});
21202128

21212129
test('generates Claude outside-voice skill for external hosts', () => {
2122-
const skillMd = path.join(hostDir, 'gstack-claude', 'SKILL.md');
2130+
const skillMd = skillFilePath('gstack-claude');
21232131
expect(fs.existsSync(skillMd)).toBe(true);
21242132
const content = fs.readFileSync(skillMd, 'utf-8');
21252133
expect(content).toContain('claude -p');
@@ -2140,7 +2148,7 @@ describe('Parameterized host smoke tests', () => {
21402148

21412149
if (hostConfig.generation.skipSkills?.includes('codex')) {
21422150
test('/codex skill excluded', () => {
2143-
expect(fs.existsSync(path.join(hostDir, 'gstack-codex', 'SKILL.md'))).toBe(false);
2151+
expect(fs.existsSync(skillFilePath('gstack-codex'))).toBe(false);
21442152
});
21452153
}
21462154
});
@@ -2159,7 +2167,8 @@ describe('--host all', () => {
21592167
// All hosts should appear in output
21602168
expect(output).toContain('FRESH: SKILL.md'); // claude
21612169
for (const hostConfig of getExternalHosts()) {
2162-
expect(output).toContain(`FRESH: ${hostConfig.hostSubdir}/skills/`);
2170+
const subdir = hostConfig.outputLayout === 'flat-agent-md' ? 'agents' : 'skills';
2171+
expect(output).toContain(`FRESH: ${hostConfig.hostSubdir}/${subdir}/`);
21632172
}
21642173
});
21652174
});
@@ -2270,9 +2279,9 @@ describe('setup script validation', () => {
22702279
expect(fnBody).toContain('rm -f "$target"');
22712280
});
22722281

2273-
test('setup supports --host auto|claude|codex|kiro|opencode', () => {
2282+
test('setup supports --host auto|claude|codex|kiro|opencode|copilot', () => {
22742283
expect(setupContent).toContain('--host');
2275-
expect(setupContent).toContain('claude|codex|kiro|factory|opencode|auto');
2284+
expect(setupContent).toContain('claude|codex|kiro|factory|opencode|copilot|auto');
22762285
});
22772286

22782287
test('auto mode detects claude, codex, kiro, and opencode binaries', () => {

test/host-config.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ const ROOT = path.resolve(import.meta.dir, '..');
3030
// ─── hosts/index.ts ─────────────────────────────────────────
3131

3232
describe('hosts/index.ts', () => {
33-
test('ALL_HOST_CONFIGS has 10 hosts', () => {
34-
expect(ALL_HOST_CONFIGS.length).toBe(10);
33+
test('ALL_HOST_CONFIGS has 11 hosts', () => {
34+
expect(ALL_HOST_CONFIGS.length).toBe(11);
3535
});
3636

3737
test('ALL_HOST_NAMES matches config names', () => {

0 commit comments

Comments
 (0)