printing-press-publish
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
Chinese/printing-press publish
/printing-press publish
Publish a generated CLI from your local library to the printing-press-library repo as a pull request.
bash
/printing-press publish notion-pp-cli
/printing-press publish notion
/printing-press publishThe public library treats
and as the source of truth for registry-display fields. Do not
edit or README catalog cells in publish PRs; the library's
post-merge workflow refreshes them from the CLI tree. Do regenerate and commit
the mirror from
because PR CI verifies mirror parity.
If a brand-new CLI's mirror is pruned because is behind, fix the
library mirror generator to discover from ; do not add a registry
entry solely to satisfy mirror parity.
library/<category>/<api-slug>/.printing-press.jsonmanifest.jsonregistry.jsoncli-skills/pp-<api-slug>/SKILL.mdlibrary/<category>/<api-slug>/SKILL.mdregistry.jsonlibrary/将本地库中生成的CLI以拉取请求(Pull Request)的形式发布到printing-press-library仓库。
bash
/printing-press publish notion-pp-cli
/printing-press publish notion
/printing-press publish公共库将和视为注册表显示字段的权威来源。请勿在发布PR中编辑或README目录单元格;仓库的合并后工作流会从CLI目录中刷新这些内容。请重新生成并提交镜像文件,该文件源自,因为PR的CI会验证镜像文件的一致性。如果全新CLI的镜像文件因过时而被移除,请修复仓库镜像生成器,使其从目录中发现内容;不要仅为满足镜像一致性而添加注册表条目。
library/<category>/<api-slug>/.printing-press.jsonmanifest.jsonregistry.jsoncli-skills/pp-<api-slug>/SKILL.mdlibrary/<category>/<api-slug>/SKILL.mdregistry.jsonlibrary/Setup
初始化设置
Before doing anything else:
<!-- PRESS_SETUP_CONTRACT_START -->
bash
undefined在执行任何操作之前:
<!-- PRESS_SETUP_CONTRACT_START -->
bash
undefinedmin-binary-version: 0.5.0
min-binary-version: 0.5.0
Derive scope first — needed for local build detection
首先获取作用域目录——本地构建检测需要用到
_scope_dir="$(git rev-parse --show-toplevel 2>/dev/null || echo "$PWD")"
_scope_dir="$(cd "$_scope_dir" && pwd -P)"
_scope_dir="$(git rev-parse --show-toplevel 2>/dev/null || echo "$PWD")"
_scope_dir="$(cd "$_scope_dir" && pwd -P)"
Prefer local build when running from inside the printing-press repo.
如果在printing-press仓库内运行,优先使用本地构建版本
if [ -x "$_scope_dir/printing-press" ] && [ -d "$_scope_dir/cmd/printing-press" ]; then
export PATH="$_scope_dir:$PATH"
echo "Using local build: $_scope_dir/printing-press"
elif ! command -v printing-press >/dev/null 2>&1; then
if [ -x "$HOME/go/bin/printing-press" ]; then
echo "printing-press found at ~/go/bin/printing-press but not on PATH."
echo "Add GOPATH/bin to your PATH: export PATH="$HOME/go/bin:$PATH""
else
echo "printing-press binary not found."
echo "Install with: go install github.com/mvanhorn/cli-printing-press/v4/cmd/printing-press@latest"
fi
return 1 2>/dev/null || exit 1
fi
PRESS_BASE="$(basename "$scope_dir" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9-]/-/g; s/^-+//; s/-+$//')"
if [ -z "$PRESS_BASE" ]; then
PRESS_BASE="workspace"
fi
PRESS_SCOPE="$PRESS_BASE-$(printf '%s' "$_scope_dir" | shasum -a 256 | cut -c1-8)"
PRESS_HOME="$HOME/printing-press"
PRESS_RUNSTATE="$PRESS_HOME/.runstate/$PRESS_SCOPE"
PRESS_LIBRARY="$PRESS_HOME/library"
PRESS_MANUSCRIPTS="$PRESS_HOME/manuscripts"
PRESS_CURRENT="$PRESS_RUNSTATE/current"
mkdir -p "$PRESS_RUNSTATE" "$PRESS_LIBRARY" "$PRESS_MANUSCRIPTS" "$PRESS_CURRENT"
<!-- PRESS_SETUP_CONTRACT_END -->
After running the setup contract, check binary version compatibility. Read the `min-binary-version` field from this skill's YAML frontmatter. Run `printing-press version --json` and parse the version from the output. Compare it to `min-binary-version` using semver rules. If the installed binary is older than the minimum, warn the user: "printing-press binary vX.Y.Z is older than the minimum required vA.B.C. Run `go install github.com/mvanhorn/cli-printing-press/v4/cmd/printing-press@latest` to update." Continue anyway but surface the warning prominently.if [ -x "$_scope_dir/printing-press" ] && [ -d "$_scope_dir/cmd/printing-press" ]; then
export PATH="$_scope_dir:$PATH"
echo "Using local build: $_scope_dir/printing-press"
elif ! command -v printing-press >/dev/null 2>&1; then
if [ -x "$HOME/go/bin/printing-press" ]; then
echo "printing-press found at ~/go/bin/printing-press but not on PATH."
echo "Add GOPATH/bin to your PATH: export PATH="$HOME/go/bin:$PATH""
else
echo "printing-press binary not found."
echo "Install with: go install github.com/mvanhorn/cli-printing-press/v4/cmd/printing-press@latest"
fi
return 1 2>/dev/null || exit 1
fi
PRESS_BASE="$(basename "$scope_dir" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9-]/-/g; s/^-+//; s/-+$//')"
if [ -z "$PRESS_BASE" ]; then
PRESS_BASE="workspace"
fi
PRESS_SCOPE="$PRESS_BASE-$(printf '%s' "$_scope_dir" | shasum -a 256 | cut -c1-8)"
PRESS_HOME="$HOME/printing-press"
PRESS_RUNSTATE="$PRESS_HOME/.runstate/$PRESS_SCOPE"
PRESS_LIBRARY="$PRESS_HOME/library"
PRESS_MANUSCRIPTS="$PRESS_HOME/manuscripts"
PRESS_CURRENT="$PRESS_RUNSTATE/current"
mkdir -p "$PRESS_RUNSTATE" "$PRESS_LIBRARY" "$PRESS_MANUSCRIPTS" "$PRESS_CURRENT"
<!-- PRESS_SETUP_CONTRACT_END -->
运行完设置脚本后,检查二进制版本兼容性。从该技能的YAML前置元数据中读取`min-binary-version`字段。运行`printing-press version --json`并解析输出中的版本号,使用语义化版本规则与`min-binary-version`进行比较。如果已安装的二进制版本低于最低要求版本,向用户发出警告:"printing-press binary vX.Y.Z is older than the minimum required vA.B.C. Run `go install github.com/mvanhorn/cli-printing-press/v4/cmd/printing-press@latest` to update."(printing-press二进制版本vX.Y.Z低于最低要求的vA.B.C版本,请运行`go install github.com/mvanhorn/cli-printing-press/v4/cmd/printing-press@latest`进行更新)。即使发出警告,仍可继续操作,但需显著展示该警告信息。Configuration
配置
PUBLISH_REPO_URL="https://github.com/mvanhorn/printing-press-library"
PUBLISH_REPO_DIR="$PRESS_HOME/.publish-repo-$PRESS_SCOPE"
PUBLISH_CONFIG="$PRESS_HOME/.publish-config-$PRESS_SCOPE.json"PUBLISH_REPO_URL="https://github.com/mvanhorn/printing-press-library"
PUBLISH_REPO_DIR="$PRESS_HOME/.publish-repo-$PRESS_SCOPE"
PUBLISH_CONFIG="$PRESS_HOME/.publish-config-$PRESS_SCOPE.json"Publish config
发布配置
$PUBLISH_CONFIGjson
{
"managed_by": "printing-press-publish",
"repo_url": "https://github.com/mvanhorn/printing-press-library",
"access": "push",
"protocol": "ssh",
"clone_path": "<home>/printing-press/.publish-repo-<scope>",
"scope_dir": "/absolute/path/to/source/worktree",
"module_path_base": "github.com/mvanhorn/printing-press-library/library"
}The field sets the Go module path prefix for published CLIs. During packaging, the full module path is constructed as . If the user wants CLIs published to a different repo or path, they edit this field.
Store expanded absolute paths for and so cleanup can
check them without relying on shell-specific expansion. The
field is required before cleanup may delete anything.
module_path_base<module_path_base>/<category>/<api-slug>clone_pathscope_dir~managed_by$PUBLISH_CONFIGjson
{
"managed_by": "printing-press-publish",
"repo_url": "https://github.com/mvanhorn/printing-press-library",
"access": "push",
"protocol": "ssh",
"clone_path": "<home>/printing-press/.publish-repo-<scope>",
"scope_dir": "/absolute/path/to/source/worktree",
"module_path_base": "github.com/mvanhorn/printing-press-library/library"
}module_path_base<module_path_base>/<category>/<api-slug>clone_pathscope_dir~managed_byScoped clone cleanup
作用域克隆清理
Before creating or reusing , prune scoped publish clones whose
source worktree no longer exists. This keeps concurrent worktrees isolated
without accumulating one library clone forever per short-lived worktree.
$PUBLISH_REPO_DIRbash
find "$PRESS_HOME" -maxdepth 1 -name '.publish-config-*.json' -type f | while read -r cfg; do
[ "$cfg" = "$PUBLISH_CONFIG" ] && continue
managed_by=$(jq -r '.managed_by // empty' "$cfg" 2>/dev/null || true)
scope_dir=$(jq -r '.scope_dir // empty' "$cfg" 2>/dev/null || true)
clone_path=$(jq -r '.clone_path // empty' "$cfg" 2>/dev/null || true)
[ "$managed_by" = "printing-press-publish" ] || continue
[ -z "$scope_dir" ] && continue
[ -e "$scope_dir" ] && continue
[ -d "$clone_path/.git" ] || continue
case "$clone_path" in "$PRESS_HOME"/.publish-repo-*) ;; *) continue ;; esac
origin=$(git -C "$clone_path" remote get-url origin 2>/dev/null || true)
case "$origin" in *mvanhorn/printing-press-library*|*/*/printing-press-library*) ;; *) continue ;; esac
[ -z "$(git -C "$clone_path" status --porcelain)" ] || continue
[ "$(git -C "$clone_path" rev-parse --abbrev-ref HEAD 2>/dev/null || true)" = "main" ] || continue
rm -rf "$clone_path" "$cfg"
done在创建或复用之前,清理那些源工作树已不存在的作用域发布克隆。这样可以保持并发工作树的隔离,避免为每个短期存在的工作树永久保留一个仓库克隆。
$PUBLISH_REPO_DIRbash
find "$PRESS_HOME" -maxdepth 1 -name '.publish-config-*.json' -type f | while read -r cfg; do
[ "$cfg" = "$PUBLISH_CONFIG" ] && continue
managed_by=$(jq -r '.managed_by // empty' "$cfg" 2>/dev/null || true)
scope_dir=$(jq -r '.scope_dir // empty' "$cfg" 2>/dev/null || true)
clone_path=$(jq -r '.clone_path // empty' "$cfg" 2>/dev/null || true)
[ "$managed_by" = "printing-press-publish" ] || continue
[ -z "$scope_dir" ] && continue
[ -e "$scope_dir" ] && continue
[ -d "$clone_path/.git" ] || continue
case "$clone_path" in "$PRESS_HOME"/.publish-repo-*) ;; *) continue ;; esac
origin=$(git -C "$clone_path" remote get-url origin 2>/dev/null || true)
case "$origin" in *mvanhorn/printing-press-library*|*/*/printing-press-library*) ;; *) continue ;; esac
[ -z "$(git -C "$clone_path" status --porcelain)" ] || continue
[ "$(git -C "$clone_path" rev-parse --abbrev-ref HEAD 2>/dev/null || true)" = "main" ] || continue
rm -rf "$clone_path" "$cfg"
doneStep 1: Prerequisites
步骤1:前置条件
Verify is authenticated:
ghbash
gh auth statusIf this fails, stop and tell the user: "GitHub CLI is not authenticated. Run first."
gh auth login验证已完成身份验证:
ghbash
gh auth status如果验证失败,停止操作并告知用户:"GitHub CLI is not authenticated. Run first."(GitHub CLI未完成身份验证,请先运行)。
gh auth logingh auth loginStep 2: Resolve API Slug
步骤2:解析API Slug
Run:
bash
printing-press library list --jsonParse the JSON output into a list of CLIs. The library is now keyed by API slug (the directory name), not CLI name.
Name resolution order (matches the score skill for consistency):
- Exact match: If the argument matches a directory name (API slug) exactly, use it
- CLI name match: If no exact match, try matching against fields, then derive the API slug from the manifest's
cli_namefieldapi_name - Suffix match: If no match yet, try against
<argument>-pp-clifieldscli_name - Glob match: If no suffix match, search for entries where or
cli_namecontains the argument as a substring. Cap at 5 most-recent matches. If multiple matches, present them via AskUserQuestion and let the user pickapi_name - No match: List all available CLIs and ask the user to pick or re-enter
- No argument: If invoked with no name, list all CLIs sorted by modification time and let the user pick
Once resolved, read the manifest's field to get the API slug. Use this slug for all downstream operations (branch names, registry entries, collision detection, path construction). The from the manifest is only used for binary-level operations.
api_namecli_nameWhen presenting matches, show the API slug and modification time in a human-friendly format (e.g., "2 hours ago", "3 days ago").
运行以下命令:
bash
printing-press library list --json将JSON输出解析为CLI列表。现在仓库以API slug(目录名称)为键,而非CLI名称。
名称解析顺序(与评分技能保持一致):
- 精确匹配:如果参数与目录名称(API slug)完全匹配,则使用该slug
- CLI名称匹配:如果没有精确匹配,尝试匹配字段,然后从manifest的
cli_name字段中推导API slugapi_name - 后缀匹配:如果仍无匹配,尝试将与
<argument>-pp-cli字段匹配cli_name - 通配符匹配:如果后缀匹配失败,搜索或
cli_name包含该参数作为子字符串的条目。最多返回5个最新匹配结果。如果有多个匹配结果,通过AskUserQuestion展示给用户,让用户选择api_name - 无匹配结果:列出所有可用的CLI,让用户选择或重新输入
- 无参数:如果调用时未提供名称,按修改时间排序列出所有CLI,让用户选择
解析完成后,读取manifest的字段获取API slug。所有后续操作(分支名称、注册表条目、冲突检测、路径构造)都使用该slug。manifest中的仅用于二进制级别的操作。
api_namecli_name展示匹配结果时,以人性化格式显示API slug和修改时间(例如:"2小时前"、"3天前")。
Step 3: Determine Category
步骤3:确定分类
Read from the resolved CLI directory.
.printing-press.jsonCategory resolution order:
-
If the manifest has afield, present it for confirmation:
category"Publishing as <category>. OK?" Give the user the option to change it -
If nobut
categoryis present, look it up:catalog_entrybashprinting-press catalog show <catalog_entry> --jsonExtract the category from the result. Present for confirmation -
If neither provides a category, present the full list via AskUserQuestion:
- developer-tools, monitoring, cloud, project-management
- productivity, social-and-messaging, sales-and-crm, marketing
- payments, auth, commerce, ai, media-and-entertainment, devices, other
从解析后的CLI目录中读取文件。
.printing-press.json分类解析顺序:
-
如果manifest中有字段,展示该字段供用户确认:
category"Publishing as <category>. OK?"(将发布到**<category>**分类。是否确认?) 允许用户选择修改分类 -
如果没有字段但存在
category,查询该条目:catalog_entrybashprinting-press catalog show <catalog_entry> --json从结果中提取分类,展示给用户确认 -
如果以上两种方式都无法获取分类,通过AskUserQuestion展示完整分类列表:
- developer-tools, monitoring, cloud, project-management
- productivity, social-and-messaging, sales-and-crm, marketing
- payments, auth, commerce, ai, media-and-entertainment, devices, other
Step 4: Validate
步骤4:验证
Run:
bash
printing-press publish validate --dir <cli-dir> --jsonParse the JSON result. Display each check result to the user:
Validating <api-slug>...
manifest PASS
phase5 PASS
go mod tidy PASS
go vet PASS
go build PASS
--help PASS
--version PASS
manuscripts WARN (no manuscripts found)If , report the failing checks and stop. Do not create a partial PR.
"passed": falseSave the field from the result — it's used in the PR description.
help_output运行以下命令:
bash
printing-press publish validate --dir <cli-dir> --json解析JSON结果。向用户展示每个检查项的结果:
Validating <api-slug>...
manifest PASS
phase5 PASS
go mod tidy PASS
go vet PASS
go build PASS
--help PASS
--version PASS
manuscripts WARN (no manuscripts found)如果,报告失败的检查项并停止操作。不要创建部分PR。
"passed": false保存结果中的字段——该字段将用于PR描述。
help_outputStep 5: Managed Clone
步骤5:托管克隆
The publish skill manages its own clone of the library repo at .
$PUBLISH_REPO_DIR发布技能会在目录下管理仓库的克隆版本。
$PUBLISH_REPO_DIRFirst-time setup
首次设置
If does not exist:
$PUBLISH_REPO_DIR-
Detect push access:bash
GH_USER=$(gh api user --jq '.login') HAS_PUSH=$(gh api repos/mvanhorn/printing-press-library --jq '.permissions.push' 2>/dev/null || echo "false") -
Detect git protocol:bash
USE_SSH=false if ssh -T git@github.com 2>&1 | grep -q "successfully authenticated"; then USE_SSH=true fi -
Clone based on access:Push access (is
HAS_PUSH):truebash# Clone directly — origin IS the upstream if [ "$USE_SSH" = "true" ]; then REPO_URL="git@github.com:mvanhorn/printing-press-library.git" else REPO_URL="https://github.com/mvanhorn/printing-press-library.git" fi git clone --depth 50 "$REPO_URL" "$PUBLISH_REPO_DIR"No push access (isHAS_PUSH):falsebash# Fork first — fail explicitly if forking is blocked if ! gh repo fork mvanhorn/printing-press-library --clone=false 2>&1; then echo "ERROR: Could not fork mvanhorn/printing-press-library." echo "The repo may restrict forking, or you may already have a fork with a different name." echo "Fork manually at https://github.com/mvanhorn/printing-press-library/fork" exit 1 fi FORK="$GH_USER/printing-press-library" # Build URLs based on protocol preference if [ "$USE_SSH" = "true" ]; then FORK_URL="git@github.com:$FORK.git" UPSTREAM_URL="git@github.com:mvanhorn/printing-press-library.git" else FORK_URL="https://github.com/$FORK.git" UPSTREAM_URL="https://github.com/mvanhorn/printing-press-library.git" fi git clone --depth 50 "$FORK_URL" "$PUBLISH_REPO_DIR" cd "$PUBLISH_REPO_DIR" git remote add upstream "$UPSTREAM_URL" git fetch upstream -
Cache the config:json
{ "managed_by": "printing-press-publish", "repo_url": "https://github.com/mvanhorn/printing-press-library", "access": "push or fork", "gh_user": "<gh username>", "protocol": "ssh or https", "clone_path": "<expanded $PUBLISH_REPO_DIR>", "scope_dir": "<absolute source worktree path>", "module_path_base": "github.com/mvanhorn/printing-press-library/library" }Write to. The$PUBLISH_CONFIGfield determines the flow for all subsequent steps. Theaccessfield is used for cross-repo PR heads. Thegh_useralways references the upstream repo (PRs land there).module_path_base
如果不存在:
$PUBLISH_REPO_DIR-
检测推送权限:bash
GH_USER=$(gh api user --jq '.login') HAS_PUSH=$(gh api repos/mvanhorn/printing-press-library --jq '.permissions.push' 2>/dev/null || echo "false") -
检测Git协议:bash
USE_SSH=false if ssh -T git@github.com 2>&1 | grep -q "successfully authenticated"; then USE_SSH=true fi -
根据权限克隆仓库:拥有推送权限(为
HAS_PUSH):truebash# 直接克隆——origin即为上游仓库 if [ "$USE_SSH" = "true" ]; then REPO_URL="git@github.com:mvanhorn/printing-press-library.git" else REPO_URL="https://github.com/mvanhorn/printing-press-library.git" fi git clone --depth 50 "$REPO_URL" "$PUBLISH_REPO_DIR"无推送权限(为HAS_PUSH):falsebash# 先fork仓库——如果fork失败则明确提示 if ! gh repo fork mvanhorn/printing-press-library --clone=false 2>&1; then echo "ERROR: Could not fork mvanhorn/printing-press-library." echo "The repo may restrict forking, or you may already have a fork with a different name." echo "Fork manually at https://github.com/mvanhorn/printing-press-library/fork" exit 1 fi FORK="$GH_USER/printing-press-library" # 根据协议偏好构建URL if [ "$USE_SSH" = "true" ]; then FORK_URL="git@github.com:$FORK.git" UPSTREAM_URL="git@github.com:mvanhorn/printing-press-library.git" else FORK_URL="https://github.com/$FORK.git" UPSTREAM_URL="https://github.com/mvanhorn/printing-press-library.git" fi git clone --depth 50 "$FORK_URL" "$PUBLISH_REPO_DIR" cd "$PUBLISH_REPO_DIR" git remote add upstream "$UPSTREAM_URL" git fetch upstream -
缓存配置:json
{ "managed_by": "printing-press-publish", "repo_url": "https://github.com/mvanhorn/printing-press-library", "access": "push or fork", "gh_user": "<gh username>", "protocol": "ssh or https", "clone_path": "<expanded $PUBLISH_REPO_DIR>", "scope_dir": "<absolute source worktree path>", "module_path_base": "github.com/mvanhorn/printing-press-library/library" }将上述内容写入。$PUBLISH_CONFIG字段决定了后续所有步骤的流程。access字段用于跨仓库PR的头部信息。gh_user始终引用上游仓库(PR将合并到该仓库)。module_path_base
Subsequent publishes
后续发布
Read , then re-check access in case it changed (user was granted push access, or access was revoked):
$PUBLISH_CONFIGbash
CURRENT_ACCESS=$(gh api repos/mvanhorn/printing-press-library --jq '.permissions.push' 2>/dev/null || echo "false")
CACHED_ACCESS=$(jq -r .access "$PUBLISH_CONFIG")
if [ "$CURRENT_ACCESS" = "true" ] && [ "$CACHED_ACCESS" = "fork" ]; then
echo "Access upgraded to push. Reconfiguring clone..."
rm -rf "$PUBLISH_REPO_DIR"
# Re-run first-time setup with push access
fi
if [ "$CURRENT_ACCESS" = "false" ] && [ "$CACHED_ACCESS" = "push" ]; then
echo "Push access revoked. Reconfiguring clone with fork..."
rm -rf "$PUBLISH_REPO_DIR"
# Re-run first-time setup with fork access
fiIf the clone was removed due to an access change, re-run first-time setup above. Otherwise, freshen the clone to match the canonical upstream:
bash
cd "$PUBLISH_REPO_DIR"
if [ "$(jq -r .access $PUBLISH_CONFIG)" = "push" ]; then
# Push access: origin IS the upstream
git fetch origin
git checkout main
git reset --hard origin/main
else
# Fork: origin is the fork, upstream is canonical
git fetch upstream
git checkout main
git reset --hard upstream/main
# Also sync origin (fork) so git push works cleanly
git push origin main --force-with-lease 2>/dev/null || true
fiVerify the clone is healthy:
bash
git rev-parse --is-inside-work-tree
test "$(git rev-parse --abbrev-ref HEAD)" = "main"If this fails, the clone is corrupt. Remove and re-run first-time setup.
$PUBLISH_REPO_DIR读取,然后重新检查权限是否发生变化(例如用户被授予推送权限,或权限被撤销):
$PUBLISH_CONFIGbash
CURRENT_ACCESS=$(gh api repos/mvanhorn/printing-press-library --jq '.permissions.push' 2>/dev/null || echo "false")
CACHED_ACCESS=$(jq -r .access "$PUBLISH_CONFIG")
if [ "$CURRENT_ACCESS" = "true" ] && [ "$CACHED_ACCESS" = "fork" ]; then
echo "Access upgraded to push. Reconfiguring clone..."
rm -rf "$PUBLISH_REPO_DIR"
# 以推送权限重新运行首次设置
fi
if [ "$CURRENT_ACCESS" = "false" ] && [ "$CACHED_ACCESS" = "push" ]; then
echo "Push access revoked. Reconfiguring clone with fork..."
rm -rf "$PUBLISH_REPO_DIR"
# 以fork权限重新运行首次设置
fi如果因权限变化而删除了克隆版本,重新运行上述首次设置步骤。否则,更新克隆版本以匹配规范的上游仓库:
bash
cd "$PUBLISH_REPO_DIR"
if [ "$(jq -r .access $PUBLISH_CONFIG)" = "push" ]; then
# 拥有推送权限:origin即为上游仓库
git fetch origin
git checkout main
git reset --hard origin/main
else
# Fork模式:origin是fork仓库,upstream是规范仓库
git fetch upstream
git checkout main
git reset --hard upstream/main
# 同时同步origin(fork仓库),确保git push可以顺利执行
git push origin main --force-with-lease 2>/dev/null || true
fi验证克隆版本是否正常:
bash
git rev-parse --is-inside-work-tree
test "$(git rev-parse --abbrev-ref HEAD)" = "main"如果验证失败,说明克隆版本已损坏。删除并重新运行首次设置。
$PUBLISH_REPO_DIRInterrupted state recovery
中断状态恢复
Before creating a new branch, check for uncommitted changes:
bash
cd "$PUBLISH_REPO_DIR"
git status --porcelainIf there are uncommitted changes, ask the user via AskUserQuestion:
- "Reset and start fresh"
- "Continue with existing changes"
If reset, run .
git checkout -- . && git clean -fd在创建新分支之前,检查是否存在未提交的更改:
bash
cd "$PUBLISH_REPO_DIR"
git status --porcelain如果存在未提交的更改,通过AskUserQuestion询问用户:
- "Reset and start fresh"(重置并重新开始)
- "Continue with existing changes"(继续使用现有更改)
如果选择重置,运行。
git checkout -- . && git clean -fdStep 6: Package
步骤6:打包
Read to get . Construct the full module path using the API slug (not the CLI name):
$PUBLISH_CONFIGmodule_path_baseMODULE_PATH="<module_path_base>/<category>/<api-slug>"For example:
github.com/mvanhorn/printing-press-library/library/productivity/notionRun with to stage the CLI into a unique temporary
directory, then copy it into the publish repo:
publish package--targetbash
PUBLISH_STAGING_ROOT="/tmp/printing-press/publish"
mkdir -p "$PUBLISH_STAGING_ROOT"
STAGING_PARENT="$(mktemp -d "$PUBLISH_STAGING_ROOT/<api-slug>-XXXXXX")"
STAGING_DIR="$STAGING_PARENT/package"
printing-press publish package \
--dir <cli-dir> \
--category <category> \
--target "$STAGING_DIR" \
--module-path "$MODULE_PATH" \
--jsonParse the JSON result. Note the , , , and . The field confirms the Go module path that was set in the packaged CLI's and import paths.
staged_dirmodule_pathmanuscripts_includedrun_idmodule_pathgo.modThen copy the staged CLI into the publish repo, replacing any existing version:
bash
undefined读取获取。使用API slug(而非CLI名称)构造完整的模块路径:
$PUBLISH_CONFIGmodule_path_baseMODULE_PATH="<module_path_base>/<category>/<api-slug>"例如:
github.com/mvanhorn/printing-press-library/library/productivity/notion运行命令并指定参数,将CLI暂存到唯一的临时目录,然后将其复制到发布仓库:
publish package--targetbash
PUBLISH_STAGING_ROOT="/tmp/printing-press/publish"
mkdir -p "$PUBLISH_STAGING_ROOT"
STAGING_PARENT="$(mktemp -d "$PUBLISH_STAGING_ROOT/<api-slug>-XXXXXX")"
STAGING_DIR="$STAGING_PARENT/package"
printing-press publish package \
--dir <cli-dir> \
--category <category> \
--target "$STAGING_DIR" \
--module-path "$MODULE_PATH" \
--json解析JSON结果。记录、、和字段。字段确认了打包后的CLI的和导入路径中设置的Go模块路径。
staged_dirmodule_pathmanuscripts_includedrun_idmodule_pathgo.mod然后将暂存的CLI复制到发布仓库,替换任何现有版本:
bash
undefinedRemove existing version (handles category changes)
删除现有版本(处理分类变更情况)
rm -rf "$PUBLISH_REPO_DIR/library"/*/"<api-slug>"
rm -rf "$PUBLISH_REPO_DIR/library"/*/"<api-slug>"
Copy staged CLI into publish repo (slug-keyed directory)
将暂存的CLI复制到发布仓库(以slug命名的目录)
cp -r "$STAGING_DIR/library/<category>/<cli-name>" "$PUBLISH_REPO_DIR/library/<category>/<api-slug>"
cp -r "$STAGING_DIR/library/<category>/<cli-name>" "$PUBLISH_REPO_DIR/library/<category>/<api-slug>"
Remove binaries (should not be committed)
删除二进制文件(不应提交)
rm -f "$PUBLISH_REPO_DIR/library/<category>/<api-slug>/<api-slug>" "$PUBLISH_REPO_DIR/library/<category>/<api-slug>/<cli-name>"
rm -f "$PUBLISH_REPO_DIR/library/<category>/<api-slug>/<api-slug>" "$PUBLISH_REPO_DIR/library/<category>/<api-slug>/<cli-name>"
Regenerate the flat cli-skills mirror from the library tree so library PR CI passes mirror parity.
从仓库目录重新生成扁平的cli-skills镜像,确保仓库PR的CI通过镜像一致性检查
if [ -f "$PUBLISH_REPO_DIR/tools/generate-skills/main.go" ]; then
(cd "$PUBLISH_REPO_DIR" && go run ./tools/generate-skills/main.go)
fi
if [ -f "$PUBLISH_REPO_DIR/tools/generate-skills/main.go" ]; then
(cd "$PUBLISH_REPO_DIR" && go run ./tools/generate-skills/main.go)
fi
Verify it builds from the publish repo
验证从发布仓库可以正常构建
cd "$PUBLISH_REPO_DIR/library/<category>/<api-slug>" && go build ./...
After the publish repo copy and build verification are complete, remove the staging
directory:
```bash
rm -rf "$STAGING_PARENT"Note: uses the CLI name (e.g., ) but the publish repo uses the API slug (e.g., ). The copy step handles this rename.
staged_direspn-pp-cliespncd "$PUBLISH_REPO_DIR/library/<category>/<api-slug>" && go build ./...
完成发布仓库的复制和构建验证后,删除临时目录:
```bash
rm -rf "$STAGING_PARENT"注意:使用CLI名称(例如:),但发布仓库使用API slug(例如:)。复制步骤会处理重命名操作。
staged_direspn-pp-cliespnStep 7: Collision Detection & Resolution
步骤7:冲突检测与解决
After the managed clone is freshened, check for name collisions before creating a branch or PR. This replaces the previous "Check for Existing PR" step.
更新托管克隆版本后,在创建分支或PR之前检查是否存在名称冲突。这将替代之前的"检查现有PR"步骤。
Detection
检测
Run these checks in sequence:
1. Check merged CLIs in managed clone:
bash
ls "$PUBLISH_REPO_DIR/library"/*/"<api-slug>" 2>/dev/nullIf found, record and note the category path.
MERGED_COLLISION=true2. Check all open PRs (any author):
bash
gh pr list --repo mvanhorn/printing-press-library --head "feat/<api-slug>" --state open --json number,title,url,authorIf the list is non-empty, record . For each PR, note the PR number, URL, and author login.
PR_COLLISION=true3. Identify own PRs:
Filter the PR list from step 2 by :
--author @meFor fork-based PRs, the head includes the username prefix:
bash
ACCESS=$(jq -r .access "$PUBLISH_CONFIG")
GH_USER=$(jq -r .gh_user "$PUBLISH_CONFIG")
if [ "$ACCESS" = "fork" ]; then
HEAD_REF="$GH_USER:feat/<api-slug>"
else
HEAD_REF="feat/<api-slug>"
fi
gh pr list --repo mvanhorn/printing-press-library --head "$HEAD_REF" --state open --author @me --json number,title,urlIf found, record , store and .
OWN_PR=trueEXISTING_PR_NUMBEREXISTING_PR_URLIf no open PR was found, also check for a previously merged PR on the same branch — by ANY author, not just yours:
bash
MERGED_PR=$(gh pr list --repo mvanhorn/printing-press-library --head "$HEAD_REF" --state merged --json number --jq '.[0].number' 2>/dev/null)If is non-empty, the branch name was already used and merged. Set so Step 8 creates a new branch name (e.g., ) instead of reusing the merged branch. Do NOT force-push onto a merged branch — would silently update a closed PR nobody is watching.
MERGED_PRBRANCH_MERGED=truefeat/<api-slug>-YYYYMMDDgh pr editThe author-agnostic lookup also catches squash-zombie branches: GitHub squash-merge leaves the source branch behind on the remote, with pre-squash commit refs that look "ahead of main" but are content-equivalent to the squash commit. Without this check, the skill misclassifies the zombie as fresh-publish, then fails because the remote branch already exists. Timestamping sidesteps the issue entirely.
git push -u按顺序运行以下检查:
1. 检查托管克隆中已合并的CLI:
bash
ls "$PUBLISH_REPO_DIR/library"/*/"<api-slug>" 2>/dev/null如果找到,记录并记录分类路径。
MERGED_COLLISION=true2. 检查所有开放PR(任何作者):
bash
gh pr list --repo mvanhorn/printing-press-library --head "feat/<api-slug>" --state open --json number,title,url,author如果列表非空,记录。对于每个PR,记录PR编号、URL和作者登录名。
PR_COLLISION=true3. 识别自己的PR:
通过过滤步骤2中的PR列表:
--author @me对于基于fork的PR,头部包含用户名前缀:
bash
ACCESS=$(jq -r .access "$PUBLISH_CONFIG")
GH_USER=$(jq -r .gh_user "$PUBLISH_CONFIG")
if [ "$ACCESS" = "fork" ]; then
HEAD_REF="$GH_USER:feat/<api-slug>"
else
HEAD_REF="feat/<api-slug>"
fi
gh pr list --repo mvanhorn/printing-press-library --head "$HEAD_REF" --state open --author @me --json number,title,url如果找到,记录,存储和。
OWN_PR=trueEXISTING_PR_NUMBEREXISTING_PR_URL如果未找到开放PR,还需检查同一分支上是否存在已合并的PR——任何作者,不仅仅是你自己:
bash
MERGED_PR=$(gh pr list --repo mvanhorn/printing-press-library --head "$HEAD_REF" --state merged --json number --jq '.[0].number' 2>/dev/null)如果非空,说明该分支名称已被使用并合并。设置,以便步骤8创建新的分支名称(例如:),而非复用已合并的分支。不要强制推送到已合并的分支——会静默更新无人关注的已关闭PR。
MERGED_PRBRANCH_MERGED=truefeat/<api-slug>-YYYYMMDDgh pr edit不区分作者的查找还能捕获** squash-zombie分支**:GitHub的 squash合并会将源分支留在远程仓库,其合并前的提交引用看起来比main分支新,但内容与squash提交等价。如果没有此检查,技能会错误地将该僵尸分支视为新发布,然后会失败,因为远程分支已存在。添加时间戳可以完全避免此问题。
git push -uNo collision
无冲突
If no merged CLI exists and no open PRs match (other than your own), set from the own-PR check (or empty if none) and proceed to Step 8 normally.
EXISTING_PR_NUMBERIf an existing open PR of yours was found, inform the user:
"Found your open PR #N for. Will update it with the new version."<api-slug>
如果不存在已合并的CLI且没有匹配的开放PR(除了你自己的PR),从自己的PR检查中设置(如果没有则为空),然后正常进入步骤8。
EXISTING_PR_NUMBER如果找到你自己的开放PR,告知用户:
"Found your open PR #N for. Will update it with the new version."(找到你针对<api-slug>的开放PR #N。将使用新版本更新该PR。)<api-slug>
Collision detected — display info
检测到冲突——展示信息
Show the user what was found:
⚠️ Name collision detected for <api-slug>
Merged: <category>/<api-slug> exists in the library
Open PR: #<number> by <author> — <url>Show all applicable lines. If , tag the PR as "(yours)".
OWN_PR=true向用户展示检测到的内容:
⚠️ Name collision detected for <api-slug>
Merged: <category>/<api-slug> exists in the library
Open PR: #<number> by <author> — <url>展示所有适用的行。如果,将PR标记为"(yours)"(你的)。
OWN_PR=trueResolution paths
解决路径
Present three options via AskUserQuestion:
If (your own open PR exists):
OWN_PR=true- Update — Update your existing PR with the new version (default, preserves current behavior)
- Alongside — Rename yours with a qualifier and publish next to the existing one
- Bail — Cancel the publish
If PR collision exists but is another user's, or merged collision only:
- Replace — Intentionally overwrite the existing CLI
- Alongside — Rename yours with a qualifier and publish next to the existing one
- Bail — Cancel the publish and view the existing CLI/PR
通过AskUserQuestion提供三个选项:
如果(存在你自己的开放PR):
OWN_PR=true- Update(更新)——使用新版本更新你现有的PR(默认选项,保留当前行为)
- Alongside(并行发布)——为你的CLI添加限定词并重命名,与现有CLI并行发布
- Bail(取消)——取消发布操作
如果存在其他用户的PR冲突,或仅存在已合并的冲突:
- Replace(替换)——有意覆盖现有CLI
- Alongside(并行发布)——为你的CLI添加限定词并重命名,与现有CLI并行发布
- Bail(取消)——取消发布操作并查看现有CLI/PR
Update path (own PR)
更新路径(自己的PR)
This is the existing update flow. Set from the detection step and proceed to Step 8, which handles force-push and PR description update.
EXISTING_PR_NUMBER这是现有的更新流程。从检测步骤中设置,然后进入步骤8,该步骤会处理强制推送和PR描述更新。
EXISTING_PR_NUMBERReplace path
替换路径
For merged CLIs or your own PR: Standard confirmation:
"This will replace the existing. Continue?"<api-slug>
For another user's PR: Stronger confirmation naming the other author:
"⚠️ This will replace's<author>(PR #N). Are you sure?"<api-slug>
If confirmed:
- The PR description must include: <api-slug>`** — <reason provided by user or "newer version">`
⚠️ **Replaces existing \ - Set (create a new PR, don't update theirs)
EXISTING_PR_NUMBER="" - Proceed to Step 8 normally
针对已合并的CLI或你自己的PR: 标准确认提示:
"This will replace the existing. Continue?"(这将替换现有的<api-slug>。是否继续?)<api-slug>
针对其他用户的PR: 更强的确认提示,提及其他作者:
"⚠️ This will replace's<author>(PR #N). Are you sure?"(⚠️ 这将替换<api-slug>的<author>(PR #N)。你确定吗?)<api-slug>
如果确认:
- PR描述必须包含:<api-slug>`** — <reason provided by user or "newer version">
⚠️ **Replaces existing \<api-slug>`** — 用户提供的原因或"新版本")(⚠️ **替换现有 - 设置(创建新PR,不更新他人的PR)
EXISTING_PR_NUMBER="" - 正常进入步骤8
Alongside path (rename)
并行发布路径(重命名)
1. Extract the original API slug from the manifest's field:
api_namebash
undefined1. 从manifest的字段中提取原始API slug:
api_namebash
undefinedRead from .printing-press.json in the publish repo's staged CLI
从发布仓库中暂存的CLI的.printing-press.json文件中读取
ORIGINAL_API_SLUG=$(cat "$PUBLISH_REPO_DIR/library/<category>/<api-slug>/.printing-press.json" | jq -r '.api_name')
**2. Generate rename suggestions** using slug format. Derive the new CLI name from the chosen slug:
- Numeric: `<api-slug>-2` (if that collides, try `-3`, `-4`, etc.)
- Non-numeric: `<api-slug>-alt`
- Custom: prompt the user for a qualifier word
After the user chooses a slug, compute:
```bash
NEW_API_SLUG="<chosen-slug>"
NEW_CLI_NAME="${NEW_API_SLUG}-pp-cli"Present the format to the user:
"Rename format:. Pick a qualifier:"<api-slug>-<qualifier>
→2<api-slug>-2 →alt<api-slug>-alt- Enter custom qualifier
3. Verify each suggestion is non-colliding before presenting:
bash
undefinedORIGINAL_API_SLUG=$(cat "$PUBLISH_REPO_DIR/library/<category>/<api-slug>/.printing-press.json" | jq -r '.api_name')
**2. 使用slug格式生成重命名建议。从选定的slug推导新的CLI名称:**
- 数字后缀:`<api-slug>-2`(如果该名称也冲突,尝试`-3`、`-4`等)
- 非数字后缀:`<api-slug>-alt`
- 自定义:提示用户输入限定词
用户选择slug后,计算:
```bash
NEW_API_SLUG="<chosen-slug>"
NEW_CLI_NAME="${NEW_API_SLUG}-pp-cli"向用户展示格式:
"Rename format:. Pick a qualifier:"(重命名格式:<api-slug>-<qualifier>。选择一个限定词:)<api-slug>-<qualifier>
→2<api-slug>-2 →alt<api-slug>-alt- 输入自定义限定词
3. 在展示建议之前,验证每个建议是否无冲突:
bash
undefinedCheck merged
检查已合并的CLI
ls "$PUBLISH_REPO_DIR/library"/*/"<suggestion>" 2>/dev/null
ls "$PUBLISH_REPO_DIR/library"/*/"<suggestion>" 2>/dev/null
Check open PRs
检查开放PR
gh pr list --repo mvanhorn/printing-press-library --head "feat/<suggestion>" --state open --json number
If a suggestion collides, skip it or increment the numeric suffix.
**4. Rename the CLI in the publish repo:**
Since Step 6 copied the staged CLI into `$PUBLISH_REPO_DIR`, the rename operates on that directory. Note: `--old-name`/`--new-name` still use CLI-name format (e.g., `dub-pp-cli`) because `RenameCLI` does content replacement — bare slugs would cause collateral damage. The `--dir` path uses the slug-keyed directory.
```bash
printing-press publish rename \
--dir "$PUBLISH_REPO_DIR/library/<category>/<api-slug>" \
--old-name <old-cli-name> \
--new-name "$NEW_CLI_NAME" \
--api-name "$ORIGINAL_API_SLUG" \
--jsonParse the JSON result. Verify . Note that should now be .
"success": truenew_dir$PUBLISH_REPO_DIR/library/<category>/$NEW_API_SLUG5. Update all downstream references for Step 8:
- Branch name: (not the old slug)
feat/$NEW_API_SLUG - PR title:
feat($NEW_API_SLUG): add $NEW_API_SLUG - Commit message:
feat($NEW_API_SLUG): add $NEW_API_SLUG - Registry.json entry: →
name$NEW_API_SLUG - Set (always a new PR for a renamed CLI)
EXISTING_PR_NUMBER=""
Proceed to Step 8 with the new name.
gh pr list --repo mvanhorn/printing-press-library --head "feat/<suggestion>" --state open --json number
如果某个建议存在冲突,跳过该建议或增加数字后缀。
**4. 在发布仓库中重命名CLI:**
由于步骤6已将暂存的CLI复制到`$PUBLISH_REPO_DIR`,重命名操作将在该目录上进行。注意:`--old-name`/`--new-name`仍使用CLI名称格式(例如:`dub-pp-cli`),因为`RenameCLI`会进行内容替换——使用纯slug会导致不必要的内容修改。`--dir`路径使用以slug命名的目录。
```bash
printing-press publish rename \
--dir "$PUBLISH_REPO_DIR/library/<category>/<api-slug>" \
--old-name <old-cli-name> \
--new-name "$NEW_CLI_NAME" \
--api-name "$ORIGINAL_API_SLUG" \
--json解析JSON结果。验证。注意现在应为。
"success": truenew_dir$PUBLISH_REPO_DIR/library/<category>/$NEW_API_SLUG5. 更新步骤8的所有下游引用:
- 分支名称:(而非旧slug)
feat/$NEW_API_SLUG - PR标题:
feat($NEW_API_SLUG): add $NEW_API_SLUG - 提交信息:
feat($NEW_API_SLUG): add $NEW_API_SLUG - Registry.json条目:→
name$NEW_API_SLUG - 设置(重命名后的CLI始终创建新PR)
EXISTING_PR_NUMBER=""
使用新名称进入步骤8。
Bail path
取消路径
Show links to what exists:
- If merged: "Existing CLI at "
library/<category>/<api-slug>/ - If open PR: "Open PR: <url>"
Exit the publish flow. If Step 6 already wrote files into , clean up with in the managed clone.
$PUBLISH_REPO_DIRgit checkout -- . && git clean -fd展示现有内容的链接:
- 如果是已合并的CLI:"Existing CLI at "(现有CLI位于
library/<category>/<api-slug>/)library/<category>/<api-slug>/ - 如果是开放PR:"Open PR: <url>"(开放PR:<url>)
退出发布流程。如果步骤6已将文件写入,在托管克隆中运行进行清理。
$PUBLISH_REPO_DIRgit checkout -- . && git clean -fdStep 8: Branch, Commit, and PR
步骤8:分支、提交与PR
Create branch
创建分支
If is set (updating an existing PR):
EXISTING_PR_NUMBERAlways overwrite the branch — the intent is clearly to update:
bash
git checkout -B feat/<api-slug>If is empty and is true (previous PR was merged):
EXISTING_PR_NUMBERBRANCH_MERGEDAuto-create a timestamped branch — do not reuse the merged branch name:
bash
git checkout -b feat/<api-slug>-$(date +%Y%m%d)If is empty and is not set (no open or merged PR):
EXISTING_PR_NUMBERBRANCH_MERGEDCheck for stale branches and competing PRs:
bash
undefined如果已设置(更新现有PR):
EXISTING_PR_NUMBER始终覆盖分支——用户的意图显然是更新:
bash
git checkout -B feat/<api-slug>如果为空且为true(之前的PR已合并):
EXISTING_PR_NUMBERBRANCH_MERGED自动创建带时间戳的分支——不要复用已合并的分支名称:
bash
git checkout -b feat/<api-slug>-$(date +%Y%m%d)如果为空且未设置(无开放或已合并的PR):
EXISTING_PR_NUMBERBRANCH_MERGED检查是否存在陈旧分支和竞争PR:
bash
undefinedCheck local and remote branches
检查本地和远程分支
LOCAL_BRANCH=$(git branch --list "feat/<api-slug>" | head -1)
REMOTE_BRANCH=$(git ls-remote --heads origin "feat/<api-slug>" 2>/dev/null | head -1)
LOCAL_BRANCH=$(git branch --list "feat/<api-slug>" | head -1)
REMOTE_BRANCH=$(git ls-remote --heads origin "feat/<api-slug>" 2>/dev/null | head -1)
If a remote branch exists, check who owns it
如果远程分支存在,检查分支所有者
if [ -n "$REMOTE_BRANCH" ]; then
Check for ANY open PR on this branch (not just ours)
OTHER_PR=$(gh pr list --repo mvanhorn/printing-press-library --head "feat/<api-slug>" --state open --json number,author --jq '.[0]' 2>/dev/null)
fi
**If another user's open PR exists on this branch** (`OTHER_PR` is non-empty and author is not `@me`):
> "Someone else has an open PR for `<api-slug>` (PR #N by @author). Creating a timestamped branch to avoid conflicts."
Auto-create a timestamped branch: `feat/<api-slug>-YYYYMMDD`. Do NOT offer to overwrite — that would stomp their work.
**If the branch exists but no competing PR** (stale branch from a previously closed/merged PR):
Ask via AskUserQuestion:
> "Found a stale branch `feat/<api-slug>` (likely from a previous publish). Overwrite it?"
- "Overwrite existing branch" — reuse the branch name
- "Create timestamped variant (feat/<api-slug>-YYYYMMDD)"
**If no branch exists:** Create normally.
```bashif [ -n "$REMOTE_BRANCH" ]; then
检查该分支上是否存在任何开放PR(不仅仅是我们的)
OTHER_PR=$(gh pr list --repo mvanhorn/printing-press-library --head "feat/<api-slug>" --state open --json number,author --jq '.[0]' 2>/dev/null)
fi
**如果该分支上存在其他用户的开放PR**(`OTHER_PR`非空且作者不是`@me`):
> "Someone else has an open PR for `<api-slug>` (PR #N by @author). Creating a timestamped branch to avoid conflicts."(其他人有针对`<api-slug>`的开放PR(PR #N,作者为@author)。将创建带时间戳的分支以避免冲突。)
自动创建带时间戳的分支:`feat/<api-slug>-YYYYMMDD`。不要提供覆盖选项——这会覆盖他人的工作。
**如果分支存在但无竞争PR**(来自之前已关闭/合并PR的陈旧分支):
通过AskUserQuestion询问用户:
> "Found a stale branch `feat/<api-slug>` (likely from a previous publish). Overwrite it?"(找到陈旧分支`feat/<api-slug>`(可能来自之前的发布)。是否覆盖该分支?)
- "Overwrite existing branch"(覆盖现有分支)——复用分支名称
- "Create timestamped variant (feat/<api-slug>-YYYYMMDD)"(创建带时间戳的变体(feat/<api-slug>-YYYYMMDD))
**如果分支不存在:** 正常创建分支。
```bashNew branch:
新分支:
git checkout -b feat/<api-slug>
git checkout -b feat/<api-slug>
Overwrite existing:
覆盖现有分支:
git checkout -B feat/<api-slug>
undefinedgit checkout -B feat/<api-slug>
undefinedCommit and push
提交与推送
bash
cd "$PUBLISH_REPO_DIR"
git add library/ cli-skills/
git commit -m "feat(<api-slug>): add <api-slug>"Push to origin (which is the fork for non-push users, or the upstream for push users):
If updating an existing PR ( is set):
EXISTING_PR_NUMBERbash
git push --force-with-lease -u origin feat/<api-slug>If creating a new PR and you chose "Overwrite existing branch" earlier:
bash
git push --force-with-lease -u origin feat/<api-slug>Otherwise (new branch, no conflicts):
bash
git push -u origin feat/<api-slug>bash
cd "$PUBLISH_REPO_DIR"
git add library/ cli-skills/
git commit -m "feat(<api-slug>): add <api-slug>"推送到origin(对于无推送权限的用户,origin是fork仓库;对于有推送权限的用户,origin是上游仓库):
如果更新现有PR(已设置):
EXISTING_PR_NUMBERbash
git push --force-with-lease -u origin feat/<api-slug>如果创建新PR且之前选择了"Overwrite existing branch":
bash
git push --force-with-lease -u origin feat/<api-slug>其他情况(新分支,无冲突):
bash
git push -u origin feat/<api-slug>Create or update PR
创建或更新PR
Read and from . These determine how is called.
accessgh_user$PUBLISH_CONFIGgh pr createFor fork-based PRs ( is ): use so GitHub creates a cross-repo PR from the fork to the upstream. Without , would try to find the branch on the upstream repo (where the user can't push) and fail.
accessfork--head <gh_user>:feat/<api-slug>--headgh pr createFor push-access PRs ( is ): use so GitHub creates the PR from the branch this flow just pushed, even when the managed clone or shell session has other branches checked out.
accesspush--head feat/<api-slug>Build the PR description from:
- The manifest (,
description,api_name,category,printing_press_version)spec_url - The captured in Step 4
help_output - The CLI's README (first 2-3 paragraphs, or note that README is missing)
- Links to and
.manuscripts/<run-id>/research/within the PR branch.manuscripts/<run-id>/proofs/ - The validation results from Step 4
- A Gaps section listing any missing manifest fields
MANDATORY: Before constructing the PR body, scrub all workspace PII. The library
repo is public. Scan any live test results, acceptance data, or manuscript excerpts
for organization names, team member names, and email addresses. Replace with generic
descriptions ("the workspace", "5 team members", "12 users"). Team keys (e.g., "ESP")
are OK but org names (e.g., "Acme Corp") are not. See
in the printing-press skill for the full policy.
references/secret-protection.mdPR description template:
markdown
undefined从读取和。这些字段决定了的调用方式。
$PUBLISH_CONFIGaccessgh_usergh pr create对于基于fork的PR(为):使用,以便GitHub从fork仓库向上游仓库创建跨仓库PR。如果不使用,会尝试在上游仓库中查找该分支(用户无法推送到上游仓库),从而失败。
accessfork--head <gh_user>:feat/<api-slug>--headgh pr create对于拥有推送权限的PR(为):使用,以便GitHub从该流程刚刚推送的分支创建PR,即使托管克隆或shell会话已切换到其他分支。
accesspush--head feat/<api-slug>从以下内容构建PR描述:
- manifest(、
description、api_name、category、printing_press_version)spec_url - 步骤4中捕获的
help_output - CLI的README(前2-3段,或注明README缺失)
- PR分支内和
.manuscripts/<run-id>/research/的链接.manuscripts/<run-id>/proofs/ - 步骤4中的验证结果
- 列出任何缺失的manifest字段的Gaps部分
强制要求:在构建PR正文之前,清理所有工作区的PII(个人可识别信息)。 该库仓库是公开的。扫描任何实时测试结果、验收数据或手稿摘录,查找组织名称、团队成员姓名和电子邮件地址。将其替换为通用描述("该工作区"、"5名团队成员"、"12名用户")。团队标识(例如:"ESP")是允许的,但组织名称(例如:"Acme Corp")不允许。有关完整政策,请查看printing-press技能中的。
references/secret-protection.mdPR描述模板:
markdown
undefined<api-slug>
<api-slug>
<If this is a Replace path, add: "⚠️ Replaces existing — <reason from user>">
<api-slug><description from manifest, or "No description available">
API: <api_name> | Category: <category> | Press version: <printing_press_version>
Spec: <spec_url or "Not specified">
<如果是替换路径,添加:"⚠️ Replaces existing — <用户提供的原因>">
<api-slug><manifest中的描述,或"No description available"(无可用描述)>
API: <api_name> | Category: <category> | Press version: <printing_press_version>
Spec: <spec_url或"Not specified"(未指定)>
CLI Shape
CLI Shape
```bash
$ <cli-name> --help
<help_output from validation>
```
```bash
$ <cli-name> --help
<help_output from validation>
```
What This CLI Does
What This CLI Does
<First 2-3 paragraphs from README.md in the CLI directory, or "README not found">
<CLI目录中README.md的前2-3段,或"README not found"(未找到README)>
Manuscripts
Manuscripts
- [Research Brief](<link to library/<category>/<api-slug>/.manuscripts/<run-id>/research/>)
- [Shipcheck Results](<link to library/<category>/<api-slug>/.manuscripts/<run-id>/proofs/>)
- [Research Brief](<library/<category>/<api-slug>/.manuscripts/<run-id>/research/>的链接)
- [Shipcheck Results](<library/<category>/<api-slug>/.manuscripts/<run-id>/proofs/>的链接)
Validation Results
Validation Results
| Check | Result |
|---|---|
| Manifest | PASS/FAIL |
| Phase 5 | PASS/FAIL |
| go mod tidy | PASS/FAIL |
| go vet | PASS/FAIL |
| go build | PASS/FAIL |
| --help | PASS/FAIL |
| --version | PASS/FAIL |
| Manuscripts | PRESENT/MISSING |
| Check | Result |
|---|---|
| Manifest | PASS/FAIL |
| Phase 5 | PASS/FAIL |
| go mod tidy | PASS/FAIL |
| go vet | PASS/FAIL |
| go build | PASS/FAIL |
| --help | PASS/FAIL |
| --version | PASS/FAIL |
| Manuscripts | PRESENT/MISSING |
Gaps
Gaps
<List any missing manifest fields, or omit this section if everything is present>
**If updating an existing PR** (`EXISTING_PR_NUMBER` is set):
```bash
cd "$PUBLISH_REPO_DIR"
gh pr edit "$EXISTING_PR_NUMBER" \
--repo mvanhorn/printing-press-library \
--body "<constructed PR body>"Display the full PR URL: "Updated PR: <EXISTING_PR_URL>" (use the full URL, not shorthand).
https://If creating a new PR:
bash
cd "$PUBLISH_REPO_DIR"<列出任何缺失的manifest字段,如果所有字段都存在则省略此部分>
**如果更新现有PR**(`EXISTING_PR_NUMBER`已设置):
```bash
cd "$PUBLISH_REPO_DIR"
gh pr edit "$EXISTING_PR_NUMBER" \
--repo mvanhorn/printing-press-library \
--body "<constructed PR body>"展示完整的PR URL:"Updated PR: <EXISTING_PR_URL>"(使用完整的 URL,而非简写格式)。
https://如果创建新PR:
bash
cd "$PUBLISH_REPO_DIR"Read access mode from config
从配置中读取访问模式
ACCESS=$(jq -r .access "$PUBLISH_CONFIG")
GH_USER=$(jq -r .gh_user "$PUBLISH_CONFIG")
if [ "$ACCESS" = "fork" ]; then
PR_HEAD_REF="$GH_USER:feat/<api-slug>"
else
PR_HEAD_REF="feat/<api-slug>"
fi
gh pr create
--repo mvanhorn/printing-press-library
--head "$PR_HEAD_REF"
--base main
--title "feat(<api-slug>): add <api-slug>"
--body "<constructed PR body>"
--repo mvanhorn/printing-press-library
--head "$PR_HEAD_REF"
--base main
--title "feat(<api-slug>): add <api-slug>"
--body "<constructed PR body>"
Display the full PR URL (e.g., `https://github.com/mvanhorn/printing-press-library/pull/10`), not the shorthand `org/repo#N` format. The full URL is clickable in all terminals and contexts.ACCESS=$(jq -r .access "$PUBLISH_CONFIG")
GH_USER=$(jq -r .gh_user "$PUBLISH_CONFIG")
if [ "$ACCESS" = "fork" ]; then
PR_HEAD_REF="$GH_USER:feat/<api-slug>"
else
PR_HEAD_REF="feat/<api-slug>"
fi
gh pr create
--repo mvanhorn/printing-press-library
--head "$PR_HEAD_REF"
--base main
--title "feat(<api-slug>): add <api-slug>"
--body "<constructed PR body>"
--repo mvanhorn/printing-press-library
--head "$PR_HEAD_REF"
--base main
--title "feat(<api-slug>): add <api-slug>"
--body "<constructed PR body>"
展示完整的PR URL(例如:`https://github.com/mvanhorn/printing-press-library/pull/10`),而非简写的`org/repo#N`格式。完整URL在所有终端和环境中都可点击。Secret & PII Protection
机密信息与PII保护
Before creating the PR, verify that no secrets leaked into the packaged CLI.
This matters because the library repo is public. A leaked API key in a PR is
a security incident — anyone can see it, even if the PR is later closed.
在创建PR之前,验证打包后的CLI中是否存在机密信息泄露。
这一点至关重要,因为该库仓库是公开的。 PR中泄露的API密钥属于安全事件——任何人都可以看到,即使PR后来被关闭。
What the Printing Press checks (deterministic)
Printing Press的检查内容(确定性)
The generation skill () runs an exact-value scan during Phase 5.5
if the user provided an API key. By the time publish runs, the Printing Press's own
mistakes should already be caught. But the user may have edited files between
generation and publish.
/printing-press如果用户提供了API密钥,生成技能()会在Phase 5.5期间运行精确值扫描。到发布流程运行时,Printing Press自身的错误应该已经被捕获。但用户可能在生成和发布之间编辑过文件。
/printing-pressWhat publish checks (best-effort, warn-only)
发布流程的检查内容(尽力而为,仅警告)
-
Ifor
gitleaksis installed, run it on the staged directory:trufflehogbashif command -v gitleaks >/dev/null 2>&1; then gitleaks detect --source "<staging-dir>/library" --no-git --verbose 2>&1 elif command -v trufflehog >/dev/null 2>&1; then trufflehog filesystem "<staging-dir>/library" 2>&1 fiThese tools use vendor-specific patterns (Steam keys, Stripe keys, GitHub tokens) with low false-positive rates. Their findings are warnings — the user reviews and decides. -
If no scanning tool is installed, do a lightweight check:
- Verify no files,
.env, orsession-state.jsonwith real credentials exist in the staged directoryconfig.toml - Check README examples use placeholders, not real values
"your-key-here" - Check manuscripts (if included) don't contain auth headers or cookie values
- Verify no
-
Never include in the staged directory:
- files
.env session-state.json- Config files with real credentials
- HAR captures with un-stripped auth headers
If any issues are found, warn the user and ask whether to proceed. The user
makes the final call — they may have intentionally included something the scan
flagged (e.g., a test fixture with a fake key). Don't block silently.
-
如果已安装或
gitleaks,在暂存目录上运行该工具:trufflehogbashif command -v gitleaks >/dev/null 2>&1; then gitleaks detect --source "<staging-dir>/library" --no-git --verbose 2>&1 elif command -v trufflehog >/dev/null 2>&1; then trufflehog filesystem "<staging-dir>/library" 2>&1 fi这些工具使用供应商特定的模式(Steam密钥、Stripe密钥、GitHub令牌),误报率低。它们的检测结果仅作为警告——由用户审核并决定是否继续。 -
如果未安装扫描工具,执行轻量级检查:
- 验证暂存目录中不存在文件、
.env或包含真实凭据的session-state.jsonconfig.toml - 检查README示例使用占位符,而非真实值
"your-key-here" - 检查手稿(如果包含)中不包含身份验证头或Cookie值
- 验证暂存目录中不存在
-
绝对不要在暂存目录中包含:
- 文件
.env session-state.json- 包含真实凭据的配置文件
- 未去除身份验证头的HAR捕获文件
如果发现任何问题,向用户发出警告并询问是否继续。最终决定权在用户手中——他们可能故意包含了扫描工具标记的内容(例如:包含假密钥的测试 fixture)。不要静默阻止操作。
PII pattern scanning (mandatory)
PII模式扫描(强制要求)
Beyond the secret scans above, run the PII pattern scanning step from
../printing-press/references/secret-protection.md
(section "PII pattern scanning"). This catches PII captured during live dogfood
that the prose guidance missed — emails, real attendee names, account
identifiers — before they ship to the public library repo.
The scan has two tiers:
- Tier 1 (auto-redact silently): vendor-prefix-anchored bearer tokens
(,
Bearer cal_live_*,Bearer sk_live_*,Bearer ghp_*, etc.). Near-zero false-positive rate.xoxp-* - Tier 2 (warn, batched user prompt): generic emails, generic bearer tokens, capitalized first+last name patterns. Allowlist suppresses spec-derived API vocabulary ("Event Types", "Booking Links") automatically.
A pre-scrub copy of the staging directory is preserved at
so the user can recover from a wrong redaction.
<staging>.pre-pii-scrub/Two prior PII leaks shipped to the public library before this scan existed.
The scan is the mechanical defense layer the prose guidance alone could not
provide.
除了上述机密信息扫描外,运行../printing-press/references/secret-protection.md中的PII模式扫描步骤("PII pattern scanning"部分)。这可以捕获在内部测试期间捕获的PII,而文字指导可能遗漏这些信息——例如电子邮件、真实参会者姓名、账户标识符——在它们被提交到公共库仓库之前。
扫描分为两个层级:
- 层级1(自动静默脱敏):供应商前缀锚定的Bearer令牌(、
Bearer cal_live_*、Bearer sk_live_*、Bearer ghp_*等)。误报率几乎为零。xoxp-* - 层级2(警告,批量用户提示):通用电子邮件、通用Bearer令牌、大写的姓名模式。自动允许列表会排除从规范中派生的API词汇("Event Types"、"Booking Links")。
暂存目录的预脱敏副本将保留在,以便用户在脱敏错误时恢复。
<staging>.pre-pii-scrub/在该扫描存在之前,已有两起PII泄露事件被提交到公共库仓库。该扫描是文字指导无法提供的机械防御层。
Error Handling
错误处理
- not authenticated: Detect in Step 1, tell user to run
ghgh auth login - CLI not found: Show available CLIs in Step 2, let user pick
- Validation fails: Show per-check results in Step 4, stop
- Repo unreachable: Report clearly in Step 5
- Fork creation fails: may fail if the user already has a fork with a different name, or if the org restricts forking. Report the error and suggest the user fork manually via the GitHub web UI.
gh repo fork - Collision check fails: If or
gh pr listcommands fail (network, auth), warn but don't block — proceed as if no collision existsls - Rename fails: Show the error from . Offer to retry with a different qualifier or bail. If the publish repo is in a partial state, reset with
publish rename --jsonbefore retryinggit checkout -- . && git clean -fd - Branch conflict (no existing PR): Ask user in Step 8 (overwrite or timestamp)
- Push fails: For fork users, ensure they're pushing to their fork (origin), not upstream. Report the error, suggest checking and
gh auth statusgit remote -v - Cross-repo PR creation fails: If fails with "head not found", the branch wasn't pushed to the fork. Verify with
gh pr create --head user:branchgit ls-remote origin feat/<api-slug>
- 未完成身份验证: 在步骤1中检测到该情况,告知用户运行
ghgh auth login - CLI未找到: 在步骤2中展示可用的CLI,让用户选择
- 验证失败: 在步骤4中展示每个检查项的结果,停止操作
- 仓库无法访问: 在步骤5中清晰报告错误
- Fork创建失败: 可能在用户已有不同名称的fork,或组织限制fork时失败。报告错误并建议用户通过GitHub网页UI手动fork仓库。
gh repo fork - 冲突检查失败: 如果或
gh pr list命令失败(网络问题、身份验证问题),发出警告但不阻止操作——按无冲突情况继续ls - 重命名失败: 展示的错误信息。提供重试不同限定词或取消操作的选项。如果发布仓库处于部分状态,在重试前运行
publish rename --json进行重置git checkout -- . && git clean -fd - 分支冲突(无现有PR): 在步骤8中询问用户(覆盖或添加时间戳)
- 推送失败: 对于fork用户,确保他们推送到自己的fork仓库(origin),而非上游仓库。报告错误,建议检查和
gh auth statusgit remote -v - 跨仓库PR创建失败: 如果失败并提示"head not found",说明分支未推送到fork仓库。使用
gh pr create --head user:branch进行验证git ls-remote origin feat/<api-slug>