feat(shared-skills): batch 55 (4 files)

This commit is contained in:
YeonGyu-Kim
2026-05-30 19:12:09 +09:00
parent ad72b80db5
commit bdb2b7b8ba
4 changed files with 330 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
import { fileURLToPath } from "node:url";
export function sharedSkillsRootPath() {
return fileURLToPath(new URL("./skills/", import.meta.url));
}
+14
View File
@@ -0,0 +1,14 @@
{
"name": "@oh-my-opencode/shared-skills",
"version": "0.1.0",
"type": "module",
"private": true,
"description": "Cross-harness SKILL.md files shared between OMO and Codex",
"exports": {
".": "./index.mjs"
},
"files": [
"index.mjs",
"skills"
]
}
@@ -0,0 +1,173 @@
#!/usr/bin/env bash
# No-excuse rule checker for Go files.
# Mirrors the philosophy of python-programmer / typescript-programmer / rust-programmer scripts:
# only rules that can be enforced via pure text matching live here.
# Everything semantic is on golangci-lint + nilaway + go test -race.
set -euo pipefail
if [ $# -eq 0 ]; then
echo "Usage: $0 <file.go> [file.go ...]" >&2
exit 2
fi
violations=0
report() {
local file="$1"
local line="$2"
local rule="$3"
local detail="$4"
echo "::error file=${file},line=${line}::[${rule}] ${detail}" >&2
violations=$((violations + 1))
}
is_test_file() {
case "$1" in
*_test.go) return 0 ;;
esac
return 1
}
is_generated_file() {
local file="$1"
case "$file" in
*.pb.go|*.connect.go|*.gen.go) return 0 ;;
*_string.go) return 0 ;;
esac
# First-line check for "Code generated ... DO NOT EDIT." (the official marker)
if [ -f "$file" ]; then
head -n 5 "$file" 2>/dev/null | grep -qE "^// Code generated .* DO NOT EDIT\.$" && return 0
fi
return 1
}
for file in "$@"; do
[ -f "$file" ] || continue
case "$file" in
*.go) ;;
*) continue ;;
esac
if is_generated_file "$file"; then
continue
fi
in_test=0
if is_test_file "$file"; then
in_test=1
fi
line_no=0
while IFS= read -r raw_line || [ -n "$raw_line" ]; do
line_no=$((line_no + 1))
line="$raw_line"
# Strip line comments before pattern checks
# (block comments are not handled — keep the rules robust to that limitation).
code_only="${line%%//*}"
# ── Exemption marker: // no-excuse-ok: <reason> ──────────────────
if [[ "$line" =~ //[[:space:]]*no-excuse-ok:[[:space:]]*.+ ]]; then
continue
fi
# ── Rule: no `_ = err` (silent error swallow) ────────────────────
# The errcheck linter catches most of these but the `_ = err` form
# specifically slips through if used with named returns.
if [[ "$code_only" =~ ^[[:space:]]*_[[:space:]]*=[[:space:]]*err[[:space:]]*$ ]] ||
[[ "$code_only" =~ ^[[:space:]]*_[[:space:]]*=[[:space:]]*err[[:space:]]*[^a-zA-Z0-9_].*$ ]]; then
if [ "$in_test" -eq 0 ]; then
report "$file" "$line_no" "silent-err" "discarding err with '_ = err' — handle the error"
fi
fi
# ── Rule: no `panic(` in non-test, non-main code ─────────────────
# Allowed in main(), allowed in tests, allowed with explicit marker.
if [[ "$code_only" =~ [^a-zA-Z0-9_]panic\( ]] || [[ "$code_only" =~ ^[[:space:]]*panic\( ]]; then
if [ "$in_test" -eq 0 ]; then
# main package main.go is the one exception
pkg_line=$(head -n 5 "$file" 2>/dev/null | grep -m1 "^package ")
if [[ "$pkg_line" != "package main" ]]; then
report "$file" "$line_no" "panic-in-lib" "panic outside main/test — return error instead"
fi
fi
fi
# ── Rule: no `log.Fatal` / `log.Panic` in library code ───────────
if [[ "$code_only" =~ log\.(Fatal|Panic)(f|ln)?\( ]]; then
if [ "$in_test" -eq 0 ]; then
pkg_line=$(head -n 5 "$file" 2>/dev/null | grep -m1 "^package ")
if [[ "$pkg_line" != "package main" ]]; then
report "$file" "$line_no" "log-fatal-in-lib" "log.Fatal/Panic outside main — return error"
fi
fi
fi
# ── Rule: no init() functions ─────────────────────────────────
# init() ruins testability and creates hidden global state.
# Exception: //go:build constraint files and generated code.
if [[ "$code_only" =~ ^func[[:space:]]+init\(\)[[:space:]]*\{ ]]; then
report "$file" "$line_no" "no-init-func" "init() ruins testability — use explicit constructor"
fi
# ── Rule: no `time.Sleep` in non-test code ──────────────────────
if [[ "$code_only" =~ time\.Sleep\( ]]; then
if [ "$in_test" -eq 0 ]; then
report "$file" "$line_no" "time-sleep" "time.Sleep in production code — use ticker/timer with ctx"
fi
fi
# ── Rule: no `context.Background()` inside functions (only in main/init/test) ──
if [[ "$code_only" =~ context\.Background\(\) ]]; then
if [ "$in_test" -eq 0 ]; then
pkg_line=$(head -n 5 "$file" 2>/dev/null | grep -m1 "^package ")
if [[ "$pkg_line" != "package main" ]]; then
report "$file" "$line_no" "ctx-background-in-lib" "context.Background() outside main — propagate ctx as parameter"
fi
fi
fi
# ── Rule: no `interface{}` (use `any`, the alias from Go 1.18+) ──
if [[ "$code_only" =~ interface\{\} ]]; then
report "$file" "$line_no" "old-interface-empty" "use 'any' instead of 'interface{}' (Go 1.18+)"
fi
# ── Rule: no bare `fmt.Println` for logging (use slog) ───────────
# Acceptable in main.go (CLI output) and tests. Reject in libraries.
if [[ "$code_only" =~ fmt\.(Print|Println|Printf)\( ]]; then
if [ "$in_test" -eq 0 ]; then
pkg_line=$(head -n 5 "$file" 2>/dev/null | grep -m1 "^package ")
if [[ "$pkg_line" != "package main" ]]; then
report "$file" "$line_no" "fmt-print-in-lib" "fmt.Print* in library — use slog for structured logs"
fi
fi
fi
# ── Rule: no `nolint` directive without reason ───────────────────
if [[ "$line" =~ //nolint(:|$| ) ]]; then
if ! [[ "$line" =~ //nolint:[a-zA-Z0-9_,-]+[[:space:]]+//[[:space:]]*[^[:space:]] ]]; then
report "$file" "$line_no" "nolint-no-reason" "//nolint requires a // reason after the linter list"
fi
fi
# ── Rule: no TODO / FIXME without an issue link or owner ─────────
# Check the full line — TODOs live in comments, which $code_only has stripped.
if echo "$line" | grep -qE '(TODO|FIXME|XXX)([[:space:]]|:)'; then
if ! echo "$line" | grep -qE '(TODO|FIXME|XXX).*[(@[]'; then
report "$file" "$line_no" "todo-no-owner" "TODO/FIXME requires (#issue) or @owner attribution"
fi
fi
done < "$file"
done
if [ "$violations" -gt 0 ]; then
echo "" >&2
echo "go-programmer: $violations violation(s). Run also:" >&2
echo " gofumpt -l ." >&2
echo " golangci-lint run --timeout 5m ./..." >&2
echo " nilaway ./..." >&2
echo " go test -race -shuffle=on -count=1 ./..." >&2
exit 1
fi
echo "go-programmer: no-excuse rules passed for $# file(s)."
@@ -0,0 +1,138 @@
#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "typer",
# "rich",
# ]
# ///
# ─── How to run ───
# 1. Install uv (if not installed):
# curl -LsSf https://astral.sh/uv/install.sh | sh
# 2. Run:
# uv run new-project.py myservice
# uv run new-project.py myservice --module github.com/your-org/myservice
# ──────────────────
#
# Creates a new Go project with the canonical strict layout:
# - go.mod with go 1.23
# - .golangci.yml (v2, strict bundle)
# - Taskfile.yml (fmt + lint + test + build)
# - cmd/server/main.go entrypoint
# - internal/{cmd,config,api,domain,obs} skeletons
# - .github/workflows/ci.yml
#
# Templates live in ./templates/ — keep this script under 250 pure LOC.
from __future__ import annotations
import subprocess
import sys
from pathlib import Path
from string import Template
import typer
from rich.console import Console
console = Console(stderr=True)
TEMPLATES_DIR = Path(__file__).parent / "templates"
def _render(template_file: str, **subs: str) -> str:
"""Read a template file and apply $placeholder substitutions.
Uses string.Template ($name) so Go/YAML curly braces stay literal.
"""
raw = (TEMPLATES_DIR / template_file).read_text()
if not subs:
return raw
return Template(raw).substitute(**subs)
# (template-file → relative output path; is_format = .format() is run)
FILES: list[tuple[str, str, bool]] = [
(".golangci.yml", ".golangci.yml", False),
("Taskfile.yml", "Taskfile.yml", False),
(".editorconfig", ".editorconfig", False),
("gitignore", ".gitignore", False),
("ci.yml", ".github/workflows/ci.yml", False),
("run.go", "internal/cmd/run.go", False),
("config.go", "internal/config/config.go", False),
("main.go.tmpl", "cmd/server/main.go", True),
("AGENTS.md.tmpl", "AGENTS.md", True),
("README.md.tmpl", "README.md", True),
]
def _init_go_module(project_dir: Path, module: str) -> None:
try:
subprocess.run(
["go", "mod", "init", module],
cwd=project_dir,
check=True,
capture_output=True,
)
console.print(f" [dim]ran[/] go mod init {module}")
except (subprocess.CalledProcessError, FileNotFoundError) as e:
console.print(f" [yellow]warn[/] go mod init failed ({e}); writing fallback go.mod")
(project_dir / "go.mod").write_text(f"module {module}\n\ngo 1.23\n")
def _create_layout(project_dir: Path) -> None:
"""Create the canonical internal/ tree."""
subdirs = [
"cmd/server",
"internal/cmd",
"internal/config",
"internal/api",
"internal/domain",
"internal/obs",
".github/workflows",
]
for sd in subdirs:
(project_dir / sd).mkdir(parents=True)
def _write_files(project_dir: Path, name: str, module: str, purpose: str) -> None:
"""Render every template into the project tree."""
for tmpl_name, out_rel, is_format in FILES:
subs = (
{"name": name, "module": module, "short_purpose": purpose}
if is_format
else {}
)
content = _render(tmpl_name, **subs)
out_path = project_dir / out_rel
out_path.parent.mkdir(parents=True, exist_ok=True)
out_path.write_text(content)
console.print(f" [dim]wrote[/] {out_rel}")
def main(
name: str,
path: str = typer.Option(".", help="Parent dir"),
module: str = typer.Option("", help="Go module path; default: <name>"),
purpose: str = typer.Option("HTTP", help="Short purpose for AGENTS.md"),
) -> None:
"""Scaffold a new Go project with the strict toolchain."""
project_dir = Path(path) / name
if project_dir.exists():
console.print(f"[red]✗[/red] {project_dir} already exists")
sys.exit(1)
module_path = module or name
project_dir.mkdir(parents=True)
_create_layout(project_dir)
_init_go_module(project_dir, module_path)
_write_files(project_dir, name, module_path, purpose)
console.print(f"\n[bold green]Done![/] cd {project_dir}")
console.print(" go get github.com/caarlos0/env/v11")
console.print(" task # fmt + lint + test")
if __name__ == "__main__":
typer.run(main)