Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions scripts/gen-skill-docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,9 @@ const HOST_ARG_VAL: HostArg = (() => {
if (val === 'codex' || val === 'agents') return 'codex';
if (val === 'factory' || val === 'droid') return 'factory';
if (val === 'claude') return 'claude';
if (val === 'opencode') return 'opencode';
if (val === 'all') return 'all';
throw new Error(`Unknown host: ${val}. Use claude, codex, factory, droid, agents, or all.`);
throw new Error(`Unknown host: ${val}. Use claude, codex, factory, droid, opencode, agents, or all.`);
})();

// For single-host mode, HOST is the host. For --host all, it's set per iteration below.
Expand Down Expand Up @@ -242,6 +243,7 @@ interface ExternalHostConfig {
const EXTERNAL_HOST_CONFIG: Record<string, ExternalHostConfig> = {
codex: { hostSubdir: '.agents', generateMetadata: true, descriptionLimit: 1024 },
factory: { hostSubdir: '.factory', generateMetadata: false },
opencode: { hostSubdir: '.opencode', generateMetadata: false },
};

// ─── Template Processing ────────────────────────────────────
Expand Down Expand Up @@ -300,8 +302,8 @@ function processExternalHost(
result = result.replace(/\.claude\/skills\/review/g, `${config.hostSubdir}/skills/gstack/review`);
result = result.replace(/\.claude\/skills/g, `${config.hostSubdir}/skills`);

// Factory-only: translate Claude Code tool names to generic phrasing
if (host === 'factory') {
// Factory/Opencode: translate Claude Code tool names to generic phrasing
if (host === 'factory' || host === 'opencode') {
result = result.replace(/use the Bash tool/g, 'run this command');
result = result.replace(/use the Write tool/g, 'create this file');
result = result.replace(/use the Read tool/g, 'read the file');
Expand Down Expand Up @@ -395,7 +397,7 @@ function findTemplates(): string[] {
return discoverTemplates(ROOT).map(t => path.join(ROOT, t.tmpl));
}

const ALL_HOSTS: Host[] = ['claude', 'codex', 'factory'];
const ALL_HOSTS: Host[] = ['claude', 'codex', 'factory', 'opencode'];
const hostsToRun: Host[] = HOST_ARG_VAL === 'all' ? ALL_HOSTS : [HOST];
const failures: { host: string; error: Error }[] = [];

Expand Down
9 changes: 8 additions & 1 deletion scripts/resolvers/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export type Host = 'claude' | 'codex' | 'factory';
export type Host = 'claude' | 'codex' | 'factory' | 'opencode';

export interface HostPaths {
skillRoot: string;
Expand Down Expand Up @@ -30,6 +30,13 @@ export const HOST_PATHS: Record<Host, HostPaths> = {
browseDir: '$GSTACK_BROWSE',
designDir: '$GSTACK_DESIGN',
},
opencode: {
skillRoot: '~/.config/opencode/skills/gstack',
localSkillRoot: '.config/opencode/skills/gstack',
binDir: '~/.config/opencode/skills/gstack/bin',
browseDir: '~/.config/opencode/skills/gstack/browse/dist',
designDir: '~/.config/opencode/skills/gstack/design/dist',
},
};

export interface TemplateContext {
Expand Down
66 changes: 62 additions & 4 deletions setup
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ CODEX_SKILLS="$HOME/.codex/skills"
CODEX_GSTACK="$CODEX_SKILLS/gstack"
FACTORY_SKILLS="$HOME/.factory/skills"
FACTORY_GSTACK="$FACTORY_SKILLS/gstack"
OPENCODE_SKILLS="$HOME/.config/opencode/skills"
OPENCODE_GSTACK="$OPENCODE_SKILLS/gstack"

IS_WINDOWS=0
case "$(uname -s)" in
Expand All @@ -34,7 +36,7 @@ SKILL_PREFIX=1
SKILL_PREFIX_FLAG=0
while [ $# -gt 0 ]; do
case "$1" in
--host) [ -z "$2" ] && echo "Missing value for --host (expected claude, codex, kiro, or auto)" >&2 && exit 1; HOST="$2"; shift 2 ;;
--host) [ -z "$2" ] && echo "Missing value for --host (expected claude, codex, kiro, factory, opencode, 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 ;;
Expand All @@ -44,8 +46,8 @@ while [ $# -gt 0 ]; do
done

case "$HOST" in
claude|codex|kiro|factory|auto) ;;
*) echo "Unknown --host value: $HOST (expected claude, codex, kiro, factory, or auto)" >&2; exit 1 ;;
claude|codex|kiro|factory|opencode|auto) ;;
*) echo "Unknown --host value: $HOST (expected claude, codex, kiro, factory, opencode, or auto)" >&2; exit 1 ;;
esac

# ─── Resolve skill prefix preference ─────────────────────────
Expand Down Expand Up @@ -104,13 +106,15 @@ INSTALL_CLAUDE=0
INSTALL_CODEX=0
INSTALL_KIRO=0
INSTALL_FACTORY=0
INSTALL_OPENCODE=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
# If none found, default to claude
if [ "$INSTALL_CLAUDE" -eq 0 ] && [ "$INSTALL_CODEX" -eq 0 ] && [ "$INSTALL_KIRO" -eq 0 ] && [ "$INSTALL_FACTORY" -eq 0 ]; then
if [ "$INSTALL_CLAUDE" -eq 0 ] && [ "$INSTALL_CODEX" -eq 0 ] && [ "$INSTALL_KIRO" -eq 0 ] && [ "$INSTALL_FACTORY" -eq 0 ] && [ "$INSTALL_OPENCODE" -eq 0 ]; then
INSTALL_CLAUDE=1
fi
elif [ "$HOST" = "claude" ]; then
Expand All @@ -121,6 +125,8 @@ elif [ "$HOST" = "kiro" ]; then
INSTALL_KIRO=1
elif [ "$HOST" = "factory" ]; then
INSTALL_FACTORY=1
elif [ "$HOST" = "opencode" ]; then
INSTALL_OPENCODE=1
fi

migrate_direct_codex_install() {
Expand Down Expand Up @@ -671,6 +677,58 @@ if [ "$INSTALL_FACTORY" -eq 1 ]; then
echo " factory skills: $FACTORY_SKILLS"
fi

# 6c. Install for Opencode (copy from .opencode/skills, rewrite paths)
if [ "$INSTALL_OPENCODE" -eq 1 ]; then
mkdir -p "$OPENCODE_SKILLS"

# Generate .opencode/ Opencode skill docs
OPENCODE_DIR="$SOURCE_GSTACK_DIR/.opencode/skills"
if [ ! -d "$OPENCODE_DIR" ]; then
echo "Generating .opencode/ skill docs..."
(
cd "$SOURCE_GSTACK_DIR"
bun install --frozen-lockfile 2>/dev/null || bun install
bun run gen:skill-docs --host opencode
)
fi

# Create gstack dir with symlinks for runtime assets
[ -L "$OPENCODE_GSTACK" ] && rm -f "$OPENCODE_GSTACK"
mkdir -p "$OPENCODE_GSTACK" "$OPENCODE_GSTACK/browse" "$OPENCODE_GSTACK/gstack-upgrade" "$OPENCODE_GSTACK/review"
ln -snf "$SOURCE_GSTACK_DIR/bin" "$OPENCODE_GSTACK/bin"
ln -snf "$SOURCE_GSTACK_DIR/browse/dist" "$OPENCODE_GSTACK/browse/dist"
ln -snf "$SOURCE_GSTACK_DIR/browse/bin" "$OPENCODE_GSTACK/browse/bin"
if [ -f "$SOURCE_GSTACK_DIR/ETHOS.md" ]; then
ln -snf "$SOURCE_GSTACK_DIR/ETHOS.md" "$OPENCODE_GSTACK/ETHOS.md"
fi

# Rewrite root SKILL.md paths for Opencode
sed -e "s|~/.claude/skills/gstack|~/.config/opencode/skills/gstack|g" \
-e "s|\.claude/skills/gstack|.config/opencode/skills/gstack|g" \
-e "s|\.claude/skills|.config/opencode/skills|g" \
"$SOURCE_GSTACK_DIR/SKILL.md" > "$OPENCODE_GSTACK/SKILL.md"

if [ ! -d "$OPENCODE_DIR" ]; then
echo " warning: no .opencode/skills/ directory found — run 'bun run gen:skill-docs --host opencode' first" >&2
else
for skill_dir in "$OPENCODE_DIR"/gstack*/; do
[ -f "$skill_dir/SKILL.md" ] || continue
skill_name="$(basename "$skill_dir")"
target_dir="$OPENCODE_SKILLS/$skill_name"
mkdir -p "$target_dir"
# Rewrite any remaining literal paths
sed -e 's|\$HOME/.codex/skills/gstack|$HOME/.config/opencode/skills/gstack|g' \
-e "s|~/.codex/skills/gstack|~/.config/opencode/skills/gstack|g" \
-e "s|~/.factory/skills/gstack|~/.config/opencode/skills/gstack|g" \
-e "s|\$GSTACK_ROOT|~/.config/opencode/skills/gstack|g" \
"$skill_dir/SKILL.md" > "$target_dir/SKILL.md"
done
echo "gstack ready (opencode)."
echo " browse: $BROWSE_BIN"
echo " opencode skills: $OPENCODE_SKILLS"
fi
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.
Expand Down
8 changes: 5 additions & 3 deletions test/gen-skill-docs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1969,15 +1969,17 @@ describe('setup script validation', () => {
expect(fnBody).toContain('ln -snf "gstack/$dir_name"');
});

test('setup supports --host auto|claude|codex|kiro', () => {
test('setup supports --host auto|claude|codex|kiro|factory|opencode', () => {
expect(setupContent).toContain('--host');
expect(setupContent).toContain('claude|codex|kiro|factory|auto');
expect(setupContent).toContain('claude|codex|kiro|factory|opencode|auto');
});

test('auto mode detects claude, codex, and kiro binaries', () => {
test('auto mode detects claude, codex, kiro, factory, and opencode binaries', () => {
expect(setupContent).toContain('command -v claude');
expect(setupContent).toContain('command -v codex');
expect(setupContent).toContain('command -v kiro-cli');
expect(setupContent).toContain('command -v droid');
expect(setupContent).toContain('command -v opencode');
});

// T1: Sidecar skip guard — prevents .agents/skills/gstack from being linked as a skill
Expand Down