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 publish
The public library treats
library/<category>/<api-slug>/.printing-press.json
and
manifest.json
as the source of truth for registry-display fields. Do not edit
registry.json
or README catalog cells in publish PRs; the library's post-merge workflow refreshes them from the CLI tree. Do regenerate and commit the
cli-skills/pp-<api-slug>/SKILL.md
mirror from
library/<category>/<api-slug>/SKILL.md
because PR CI verifies mirror parity. If a brand-new CLI's mirror is pruned because
registry.json
is behind, fix the library mirror generator to discover from
library/
; do not add a registry entry solely to satisfy mirror parity.
将本地库中生成的CLI以拉取请求(Pull Request)的形式发布到printing-press-library仓库。
bash
/printing-press publish notion-pp-cli
/printing-press publish notion
/printing-press publish
公共库将
library/<category>/<api-slug>/.printing-press.json
manifest.json
视为注册表显示字段的权威来源。请勿在发布PR中编辑
registry.json
或README目录单元格;仓库的合并后工作流会从CLI目录中刷新这些内容。请重新生成并提交
cli-skills/pp-<api-slug>/SKILL.md
镜像文件,该文件源自
library/<category>/<api-slug>/SKILL.md
,因为PR的CI会验证镜像文件的一致性。如果全新CLI的镜像文件因
registry.json
过时而被移除,请修复仓库镜像生成器,使其从
library/
目录中发现内容;不要仅为满足镜像一致性而添加注册表条目。

Setup

初始化设置

Before doing anything else:
<!-- PRESS_SETUP_CONTRACT_START -->
bash
undefined
在执行任何操作之前:
<!-- PRESS_SETUP_CONTRACT_START -->
bash
undefined

min-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_CONFIG
stores persistent publish settings as JSON. On first publish, create it with defaults. The user can edit it to change the library repo or module path base.
json
{
  "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
module_path_base
field sets the Go module path prefix for published CLIs. During packaging, the full module path is constructed as
<module_path_base>/<category>/<api-slug>
. If the user wants CLIs published to a different repo or path, they edit this field. Store expanded absolute paths for
clone_path
and
scope_dir
so cleanup can check them without relying on shell-specific
~
expansion. The
managed_by
field is required before cleanup may delete anything.
$PUBLISH_CONFIG
以JSON格式存储持久化的发布设置。首次发布时,使用默认值创建该文件。用户可通过编辑该文件来更改仓库地址或模块路径前缀。
json
{
  "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
字段为发布的CLI设置Go模块路径前缀。打包时,完整的模块路径将构造成
<module_path_base>/<category>/<api-slug>
。如果用户希望将CLI发布到其他仓库或路径,可编辑此字段。为
clone_path
scope_dir
存储展开后的绝对路径,以便清理操作无需依赖shell特定的
~
展开即可检查这些路径。
managed_by
字段是清理操作允许删除内容的必要条件。

Scoped clone cleanup

作用域克隆清理

Before creating or reusing
$PUBLISH_REPO_DIR
, 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.
bash
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_DIR
之前,清理那些源工作树已不存在的作用域发布克隆。这样可以保持并发工作树的隔离,避免为每个短期存在的工作树永久保留一个仓库克隆。
bash
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

Step 1: Prerequisites

步骤1:前置条件

Verify
gh
is authenticated:
bash
gh auth status
If this fails, stop and tell the user: "GitHub CLI is not authenticated. Run
gh auth login
first."
验证
gh
已完成身份验证:
bash
gh auth status
如果验证失败,停止操作并告知用户:"GitHub CLI is not authenticated. Run
gh auth login
first."(GitHub CLI未完成身份验证,请先运行
gh auth login
)。

Step 2: Resolve API Slug

步骤2:解析API Slug

Run:
bash
printing-press library list --json
Parse 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):
  1. Exact match: If the argument matches a directory name (API slug) exactly, use it
  2. CLI name match: If no exact match, try matching against
    cli_name
    fields, then derive the API slug from the manifest's
    api_name
    field
  3. Suffix match: If no match yet, try
    <argument>-pp-cli
    against
    cli_name
    fields
  4. Glob match: If no suffix match, search for entries where
    cli_name
    or
    api_name
    contains the argument as a substring. Cap at 5 most-recent matches. If multiple matches, present them via AskUserQuestion and let the user pick
  5. No match: List all available CLIs and ask the user to pick or re-enter
  6. 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
api_name
field to get the API slug. Use this slug for all downstream operations (branch names, registry entries, collision detection, path construction). The
cli_name
from the manifest is only used for binary-level operations.
When 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名称。
名称解析顺序(与评分技能保持一致):
  1. 精确匹配:如果参数与目录名称(API slug)完全匹配,则使用该slug
  2. CLI名称匹配:如果没有精确匹配,尝试匹配
    cli_name
    字段,然后从manifest的
    api_name
    字段中推导API slug
  3. 后缀匹配:如果仍无匹配,尝试将
    <argument>-pp-cli
    cli_name
    字段匹配
  4. 通配符匹配:如果后缀匹配失败,搜索
    cli_name
    api_name
    包含该参数作为子字符串的条目。最多返回5个最新匹配结果。如果有多个匹配结果,通过AskUserQuestion展示给用户,让用户选择
  5. 无匹配结果:列出所有可用的CLI,让用户选择或重新输入
  6. 无参数:如果调用时未提供名称,按修改时间排序列出所有CLI,让用户选择
解析完成后,读取manifest的
api_name
字段获取API slug。所有后续操作(分支名称、注册表条目、冲突检测、路径构造)都使用该slug。manifest中的
cli_name
仅用于二进制级别的操作。
展示匹配结果时,以人性化格式显示API slug和修改时间(例如:"2小时前"、"3天前")。

Step 3: Determine Category

步骤3:确定分类

Read
.printing-press.json
from the resolved CLI directory.
Category resolution order:
  1. If the manifest has a
    category
    field, present it for confirmation:
    "Publishing as <category>. OK?" Give the user the option to change it
  2. If no
    category
    but
    catalog_entry
    is present, look it up:
    bash
    printing-press catalog show <catalog_entry> --json
    Extract the category from the result. Present for confirmation
  3. 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
文件。
分类解析顺序
  1. 如果manifest中有
    category
    字段,展示该字段供用户确认:
    "Publishing as <category>. OK?"(将发布到**<category>**分类。是否确认?) 允许用户选择修改分类
  2. 如果没有
    category
    字段但存在
    catalog_entry
    ,查询该条目:
    bash
    printing-press catalog show <catalog_entry> --json
    从结果中提取分类,展示给用户确认
  3. 如果以上两种方式都无法获取分类,通过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> --json
Parse 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
"passed": false
, report the failing checks and stop. Do not create a partial PR.
Save the
help_output
field from the result — it's used in the PR description.
运行以下命令:
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)
如果
"passed": false
,报告失败的检查项并停止操作。不要创建部分PR。
保存结果中的
help_output
字段——该字段将用于PR描述。

Step 5: Managed Clone

步骤5:托管克隆

The publish skill manages its own clone of the library repo at
$PUBLISH_REPO_DIR
.
发布技能会在
$PUBLISH_REPO_DIR
目录下管理仓库的克隆版本。

First-time setup

首次设置

If
$PUBLISH_REPO_DIR
does not exist:
  1. 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")
  2. Detect git protocol:
    bash
    USE_SSH=false
    if ssh -T git@github.com 2>&1 | grep -q "successfully authenticated"; then
      USE_SSH=true
    fi
  3. Clone based on access:
    Push access (
    HAS_PUSH
    is
    true
    ):
    bash
    # 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 (
    HAS_PUSH
    is
    false
    ):
    bash
    # 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
  4. 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
    $PUBLISH_CONFIG
    . The
    access
    field determines the flow for all subsequent steps. The
    gh_user
    field is used for cross-repo PR heads. The
    module_path_base
    always references the upstream repo (PRs land there).
如果
$PUBLISH_REPO_DIR
不存在:
  1. 检测推送权限
    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")
  2. 检测Git协议
    bash
    USE_SSH=false
    if ssh -T git@github.com 2>&1 | grep -q "successfully authenticated"; then
      USE_SSH=true
    fi
  3. 根据权限克隆仓库
    拥有推送权限
    HAS_PUSH
    true
    ):
    bash
    # 直接克隆——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
    false
    ):
    bash
    # 先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
  4. 缓存配置
    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
    字段决定了后续所有步骤的流程。
    gh_user
    字段用于跨仓库PR的头部信息。
    module_path_base
    始终引用上游仓库(PR将合并到该仓库)。

Subsequent publishes

后续发布

Read
$PUBLISH_CONFIG
, then re-check access in case it changed (user was granted push access, or access was revoked):
bash
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
fi
If 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
fi
Verify 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
$PUBLISH_REPO_DIR
and re-run first-time setup.
读取
$PUBLISH_CONFIG
,然后重新检查权限是否发生变化(例如用户被授予推送权限,或权限被撤销):
bash
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_DIR
并重新运行首次设置。

Interrupted state recovery

中断状态恢复

Before creating a new branch, check for uncommitted changes:
bash
cd "$PUBLISH_REPO_DIR"
git status --porcelain
If 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 -fd

Step 6: Package

步骤6:打包

Read
$PUBLISH_CONFIG
to get
module_path_base
. Construct the full module path using the API slug (not the CLI name):
MODULE_PATH="<module_path_base>/<category>/<api-slug>"
For example:
github.com/mvanhorn/printing-press-library/library/productivity/notion
Run
publish package
with
--target
to stage the CLI into a unique temporary directory, then copy it into the publish repo:
bash
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
Parse the JSON result. Note the
staged_dir
,
module_path
,
manuscripts_included
, and
run_id
. The
module_path
field confirms the Go module path that was set in the packaged CLI's
go.mod
and import paths.
Then copy the staged CLI into the publish repo, replacing any existing version:
bash
undefined
读取
$PUBLISH_CONFIG
获取
module_path_base
。使用API slug(而非CLI名称)构造完整的模块路径:
MODULE_PATH="<module_path_base>/<category>/<api-slug>"
例如:
github.com/mvanhorn/printing-press-library/library/productivity/notion
运行
publish package
命令并指定
--target
参数,将CLI暂存到唯一的临时目录,然后将其复制到发布仓库:
bash
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结果。记录
staged_dir
module_path
manuscripts_included
run_id
字段。
module_path
字段确认了打包后的CLI的
go.mod
和导入路径中设置的Go模块路径。
然后将暂存的CLI复制到发布仓库,替换任何现有版本:
bash
undefined

Remove 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:
staged_dir
uses the CLI name (e.g.,
espn-pp-cli
) but the publish repo uses the API slug (e.g.,
espn
). The copy step handles this rename.
cd "$PUBLISH_REPO_DIR/library/<category>/<api-slug>" && go build ./...

完成发布仓库的复制和构建验证后,删除临时目录:

```bash
rm -rf "$STAGING_PARENT"
注意:
staged_dir
使用CLI名称(例如:
espn-pp-cli
),但发布仓库使用API slug(例如:
espn
)。复制步骤会处理重命名操作。

Step 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/null
If found, record
MERGED_COLLISION=true
and note the category path.
2. 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,author
If the list is non-empty, record
PR_COLLISION=true
. For each PR, note the PR number, URL, and author login.
3. Identify own PRs:
Filter the PR list from step 2 by
--author @me
:
For 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,url
If found, record
OWN_PR=true
, store
EXISTING_PR_NUMBER
and
EXISTING_PR_URL
.
If 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
MERGED_PR
is non-empty, the branch name was already used and merged. Set
BRANCH_MERGED=true
so Step 8 creates a new branch name (e.g.,
feat/<api-slug>-YYYYMMDD
) instead of reusing the merged branch. Do NOT force-push onto a merged branch —
gh pr edit
would silently update a closed PR nobody is watching.
The 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
git push -u
fails because the remote branch already exists. Timestamping sidesteps the issue entirely.
按顺序运行以下检查:
1. 检查托管克隆中已合并的CLI:
bash
ls "$PUBLISH_REPO_DIR/library"/*/"<api-slug>" 2>/dev/null
如果找到,记录
MERGED_COLLISION=true
并记录分类路径。
2. 检查所有开放PR(任何作者):
bash
gh pr list --repo mvanhorn/printing-press-library --head "feat/<api-slug>" --state open --json number,title,url,author
如果列表非空,记录
PR_COLLISION=true
。对于每个PR,记录PR编号、URL和作者登录名。
3. 识别自己的PR:
通过
--author @me
过滤步骤2中的PR列表:
对于基于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=true
,存储
EXISTING_PR_NUMBER
EXISTING_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)
如果
MERGED_PR
非空,说明该分支名称已被使用并合并。设置
BRANCH_MERGED=true
,以便步骤8创建新的分支名称(例如:
feat/<api-slug>-YYYYMMDD
),而非复用已合并的分支。不要强制推送到已合并的分支——
gh pr edit
会静默更新无人关注的已关闭PR。
不区分作者的查找还能捕获** squash-zombie分支**:GitHub的 squash合并会将源分支留在远程仓库,其合并前的提交引用看起来比main分支新,但内容与squash提交等价。如果没有此检查,技能会错误地将该僵尸分支视为新发布,然后
git push -u
会失败,因为远程分支已存在。添加时间戳可以完全避免此问题。

No collision

无冲突

If no merged CLI exists and no open PRs match (other than your own), set
EXISTING_PR_NUMBER
from the own-PR check (or empty if none) and proceed to Step 8 normally.
If an existing open PR of yours was found, inform the user:
"Found your open PR #N for
<api-slug>
. Will update it with the new version."
如果不存在已合并的CLI且没有匹配的开放PR(除了你自己的PR),从自己的PR检查中设置
EXISTING_PR_NUMBER
(如果没有则为空),然后正常进入步骤8。
如果找到你自己的开放PR,告知用户:
"Found your open PR #N for
<api-slug>
. Will update it with the new version."(找到你针对
<api-slug>
的开放PR #N。将使用新版本更新该PR。)

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
OWN_PR=true
, tag the PR as "(yours)".
向用户展示检测到的内容:
⚠️  Name collision detected for <api-slug>

  Merged: <category>/<api-slug> exists in the library
  Open PR: #<number> by <author> — <url>
展示所有适用的行。如果
OWN_PR=true
,将PR标记为"(yours)"(你的)。

Resolution paths

解决路径

Present three options via AskUserQuestion:
If
OWN_PR=true
(your own open PR exists):
  • 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提供三个选项:
如果
OWN_PR=true
(存在你自己的开放PR):
  • 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
EXISTING_PR_NUMBER
from the detection step and proceed to Step 8, which handles force-push and PR description update.
这是现有的更新流程。从检测步骤中设置
EXISTING_PR_NUMBER
,然后进入步骤8,该步骤会处理强制推送和PR描述更新。

Replace path

替换路径

For merged CLIs or your own PR: Standard confirmation:
"This will replace the existing
<api-slug>
. Continue?"
For another user's PR: Stronger confirmation naming the other author:
"⚠️ This will replace
<author>
's
<api-slug>
(PR #N). Are you sure?"
If confirmed:
  • The PR description must include:
    ⚠️ **Replaces existing \
    <api-slug>`** — <reason provided by user or "newer version">`
  • Set
    EXISTING_PR_NUMBER=""
    (create a new PR, don't update theirs)
  • Proceed to Step 8 normally
针对已合并的CLI或你自己的PR: 标准确认提示:
"This will replace the existing
<api-slug>
. Continue?"(这将替换现有的
<api-slug>
。是否继续?)
针对其他用户的PR: 更强的确认提示,提及其他作者:
"⚠️ This will replace
<author>
's
<api-slug>
(PR #N). Are you sure?"(⚠️ 这将替换
<author>
<api-slug>
(PR #N)。你确定吗?)
如果确认:
  • PR描述必须包含:
    ⚠️ **Replaces existing \
    <api-slug>`** — <reason provided by user or "newer version">
    (⚠️ **替换现有
    <api-slug>`** — 用户提供的原因或"新版本")
  • 设置
    EXISTING_PR_NUMBER=""
    (创建新PR,不更新他人的PR)
  • 正常进入步骤8

Alongside path (rename)

并行发布路径(重命名)

1. Extract the original API slug from the manifest's
api_name
field:
bash
undefined
1. 从manifest的
api_name
字段中提取原始API slug:
bash
undefined

Read 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:
<api-slug>-<qualifier>
. Pick a qualifier:"
  1. 2
    <api-slug>-2
  2. alt
    <api-slug>-alt
  3. Enter custom qualifier
3. Verify each suggestion is non-colliding before presenting:
bash
undefined
ORIGINAL_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:
<api-slug>-<qualifier>
. Pick a qualifier:"(重命名格式:
<api-slug>-<qualifier>
。选择一个限定词:)
  1. 2
    <api-slug>-2
  2. alt
    <api-slug>-alt
  3. 输入自定义限定词
3. 在展示建议之前,验证每个建议是否无冲突:
bash
undefined

Check 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" \
  --json
Parse the JSON result. Verify
"success": true
. Note that
new_dir
should now be
$PUBLISH_REPO_DIR/library/<category>/$NEW_API_SLUG
.
5. Update all downstream references for Step 8:
  • Branch name:
    feat/$NEW_API_SLUG
    (not the old 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
    EXISTING_PR_NUMBER=""
    (always a new PR for a renamed CLI)
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": true
。注意
new_dir
现在应为
$PUBLISH_REPO_DIR/library/<category>/$NEW_API_SLUG
5. 更新步骤8的所有下游引用:
  • 分支名称:
    feat/$NEW_API_SLUG
    (而非旧slug)
  • PR标题:
    feat($NEW_API_SLUG): add $NEW_API_SLUG
  • 提交信息:
    feat($NEW_API_SLUG): add $NEW_API_SLUG
  • Registry.json条目:
    name
    $NEW_API_SLUG
  • 设置
    EXISTING_PR_NUMBER=""
    (重命名后的CLI始终创建新PR)
使用新名称进入步骤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
$PUBLISH_REPO_DIR
, clean up with
git checkout -- . && git clean -fd
in the managed clone.
展示现有内容的链接:
  • 如果是已合并的CLI:"Existing CLI at
    library/<category>/<api-slug>/
    "(现有CLI位于
    library/<category>/<api-slug>/
  • 如果是开放PR:"Open PR: <url>"(开放PR:<url>
退出发布流程。如果步骤6已将文件写入
$PUBLISH_REPO_DIR
,在托管克隆中运行
git checkout -- . && git clean -fd
进行清理。

Step 8: Branch, Commit, and PR

步骤8:分支、提交与PR

Create branch

创建分支

If
EXISTING_PR_NUMBER
is set
(updating an existing PR):
Always overwrite the branch — the intent is clearly to update:
bash
git checkout -B feat/<api-slug>
If
EXISTING_PR_NUMBER
is empty and
BRANCH_MERGED
is true
(previous PR was merged):
Auto-create a timestamped branch — do not reuse the merged branch name:
bash
git checkout -b feat/<api-slug>-$(date +%Y%m%d)
If
EXISTING_PR_NUMBER
is empty and
BRANCH_MERGED
is not set
(no open or merged PR):
Check for stale branches and competing PRs:
bash
undefined
如果
EXISTING_PR_NUMBER
已设置
(更新现有PR):
始终覆盖分支——用户的意图显然是更新:
bash
git checkout -B feat/<api-slug>
如果
EXISTING_PR_NUMBER
为空且
BRANCH_MERGED
为true
(之前的PR已合并):
自动创建带时间戳的分支——不要复用已合并的分支名称:
bash
git checkout -b feat/<api-slug>-$(date +%Y%m%d)
如果
EXISTING_PR_NUMBER
为空且
BRANCH_MERGED
未设置
(无开放或已合并的PR):
检查是否存在陈旧分支和竞争PR:
bash
undefined

Check 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.

```bash
if [ -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))

**如果分支不存在:** 正常创建分支。

```bash

New branch:

新分支:

git checkout -b feat/<api-slug>
git checkout -b feat/<api-slug>

Overwrite existing:

覆盖现有分支:

git checkout -B feat/<api-slug>
undefined
git checkout -B feat/<api-slug>
undefined

Commit 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 (
EXISTING_PR_NUMBER
is set):
bash
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_NUMBER
已设置):
bash
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
access
and
gh_user
from
$PUBLISH_CONFIG
. These determine how
gh pr create
is called.
For fork-based PRs (
access
is
fork
): use
--head <gh_user>:feat/<api-slug>
so GitHub creates a cross-repo PR from the fork to the upstream. Without
--head
,
gh pr create
would try to find the branch on the upstream repo (where the user can't push) and fail.
For push-access PRs (
access
is
push
): use
--head feat/<api-slug>
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.
Build the PR description from:
  • The manifest (
    description
    ,
    api_name
    ,
    category
    ,
    printing_press_version
    ,
    spec_url
    )
  • The
    help_output
    captured in Step 4
  • The CLI's README (first 2-3 paragraphs, or note that README is missing)
  • Links to
    .manuscripts/<run-id>/research/
    and
    .manuscripts/<run-id>/proofs/
    within the PR branch
  • 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
references/secret-protection.md
in the printing-press skill for the full policy.
PR description template:
markdown
undefined
$PUBLISH_CONFIG
读取
access
gh_user
。这些字段决定了
gh pr create
的调用方式。
对于基于fork的PR
access
fork
):使用
--head <gh_user>:feat/<api-slug>
,以便GitHub从fork仓库向上游仓库创建跨仓库PR。如果不使用
--head
gh pr create
会尝试在上游仓库中查找该分支(用户无法推送到上游仓库),从而失败。
对于拥有推送权限的PR
access
push
):使用
--head feat/<api-slug>
,以便GitHub从该流程刚刚推送的分支创建PR,即使托管克隆或shell会话已切换到其他分支。
从以下内容构建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.md
PR描述模板:
markdown
undefined

<api-slug>

<api-slug>

<If this is a Replace path, add: "⚠️ Replaces existing
<api-slug>
<reason from user>">
<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

CheckResult
ManifestPASS/FAIL
Phase 5PASS/FAIL
go mod tidyPASS/FAIL
go vetPASS/FAIL
go buildPASS/FAIL
--helpPASS/FAIL
--versionPASS/FAIL
ManuscriptsPRESENT/MISSING
CheckResult
ManifestPASS/FAIL
Phase 5PASS/FAIL
go mod tidyPASS/FAIL
go vetPASS/FAIL
go buildPASS/FAIL
--helpPASS/FAIL
--versionPASS/FAIL
ManuscriptsPRESENT/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
https://
URL, not shorthand).
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>"(使用完整的
https://
URL,而非简写格式)。
如果创建新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>"

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>"

展示完整的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 (
/printing-press
) 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.
如果用户提供了API密钥,生成技能(
/printing-press
)会在Phase 5.5期间运行精确值扫描。到发布流程运行时,Printing Press自身的错误应该已经被捕获。但用户可能在生成和发布之间编辑过文件。

What publish checks (best-effort, warn-only)

发布流程的检查内容(尽力而为,仅警告)

  1. If
    gitleaks
    or
    trufflehog
    is installed
    , run it on the staged directory:
    bash
    if 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
    These 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.
  2. If no scanning tool is installed, do a lightweight check:
    • Verify no
      .env
      files,
      session-state.json
      , or
      config.toml
      with real credentials exist in the staged directory
    • Check README examples use
      "your-key-here"
      placeholders, not real values
    • Check manuscripts (if included) don't contain auth headers or cookie values
  3. Never include in the staged directory:
    • .env
      files
    • 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.
  1. 如果已安装
    gitleaks
    trufflehog
    ,在暂存目录上运行该工具:
    bash
    if 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令牌),误报率低。它们的检测结果仅作为警告——由用户审核并决定是否继续。
  2. 如果未安装扫描工具,执行轻量级检查:
    • 验证暂存目录中不存在
      .env
      文件、
      session-state.json
      或包含真实凭据的
      config.toml
    • 检查README示例使用
      "your-key-here"
      占位符,而非真实值
    • 检查手稿(如果包含)中不包含身份验证头或Cookie值
  3. 绝对不要在暂存目录中包含
    • .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_*
    ,
    xoxp-*
    , etc.). Near-zero false-positive rate.
  • 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
<staging>.pre-pii-scrub/
so the user can recover from a wrong redaction.
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

错误处理

  • gh
    not authenticated:
    Detect in Step 1, tell user to run
    gh 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:
    gh repo fork
    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.
  • Collision check fails: If
    gh pr list
    or
    ls
    commands fail (network, auth), warn but don't block — proceed as if no collision exists
  • Rename fails: Show the error from
    publish rename --json
    . Offer to retry with a different qualifier or bail. If the publish repo is in a partial state, reset with
    git checkout -- . && git clean -fd
    before retrying
  • 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
    gh auth status
    and
    git remote -v
  • Cross-repo PR creation fails: If
    gh pr create --head user:branch
    fails with "head not found", the branch wasn't pushed to the fork. Verify with
    git ls-remote origin feat/<api-slug>
  • gh
    未完成身份验证:
    在步骤1中检测到该情况,告知用户运行
    gh auth login
  • CLI未找到: 在步骤2中展示可用的CLI,让用户选择
  • 验证失败: 在步骤4中展示每个检查项的结果,停止操作
  • 仓库无法访问: 在步骤5中清晰报告错误
  • Fork创建失败:
    gh repo fork
    可能在用户已有不同名称的fork,或组织限制fork时失败。报告错误并建议用户通过GitHub网页UI手动fork仓库。
  • 冲突检查失败: 如果
    gh pr list
    ls
    命令失败(网络问题、身份验证问题),发出警告但不阻止操作——按无冲突情况继续
  • 重命名失败: 展示
    publish rename --json
    的错误信息。提供重试不同限定词或取消操作的选项。如果发布仓库处于部分状态,在重试前运行
    git checkout -- . && git clean -fd
    进行重置
  • 分支冲突(无现有PR): 在步骤8中询问用户(覆盖或添加时间戳)
  • 推送失败: 对于fork用户,确保他们推送到自己的fork仓库(origin),而非上游仓库。报告错误,建议检查
    gh auth status
    git remote -v
  • 跨仓库PR创建失败: 如果
    gh pr create --head user:branch
    失败并提示"head not found",说明分支未推送到fork仓库。使用
    git ls-remote origin feat/<api-slug>
    进行验证