From bdb2b7b8ba9c8e6234cb95650a17e1095533700d Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Sat, 30 May 2026 19:12:09 +0900 Subject: [PATCH] feat(shared-skills): batch 55 (4 files) --- packages/shared-skills/index.mjs | 5 + packages/shared-skills/package.json | 14 ++ .../scripts/go/check-no-excuse-rules.sh | 173 ++++++++++++++++++ .../programming/scripts/go/new-project.py | 138 ++++++++++++++ 4 files changed, 330 insertions(+) create mode 100644 packages/shared-skills/index.mjs create mode 100644 packages/shared-skills/package.json create mode 100755 packages/shared-skills/skills/programming/scripts/go/check-no-excuse-rules.sh create mode 100755 packages/shared-skills/skills/programming/scripts/go/new-project.py diff --git a/packages/shared-skills/index.mjs b/packages/shared-skills/index.mjs new file mode 100644 index 000000000..f8d5d67f4 --- /dev/null +++ b/packages/shared-skills/index.mjs @@ -0,0 +1,5 @@ +import { fileURLToPath } from "node:url"; + +export function sharedSkillsRootPath() { + return fileURLToPath(new URL("./skills/", import.meta.url)); +} diff --git a/packages/shared-skills/package.json b/packages/shared-skills/package.json new file mode 100644 index 000000000..eac0a8ecc --- /dev/null +++ b/packages/shared-skills/package.json @@ -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" + ] +} diff --git a/packages/shared-skills/skills/programming/scripts/go/check-no-excuse-rules.sh b/packages/shared-skills/skills/programming/scripts/go/check-no-excuse-rules.sh new file mode 100755 index 000000000..7455dc426 --- /dev/null +++ b/packages/shared-skills/skills/programming/scripts/go/check-no-excuse-rules.sh @@ -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 ...]" >&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: ────────────────── + 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)." diff --git a/packages/shared-skills/skills/programming/scripts/go/new-project.py b/packages/shared-skills/skills/programming/scripts/go/new-project.py new file mode 100755 index 000000000..6a5f94628 --- /dev/null +++ b/packages/shared-skills/skills/programming/scripts/go/new-project.py @@ -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: "), + 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)