Add branching-strategy skill with next and create actions

Version-aware branch naming (X.Y.Z.Q/{TASK_CODE}-{FEATURE}-{SEQ}),
branch creation from origin/test with origin/main fallback, and the
PR-into-test merge rule. Ships a TypeScript CLI (next, create) plus
shell modules (loggers, base, cli) run via Taskfile.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-24 13:48:13 +03:00
co-authored by Claude Fable 5
parent 823baf8302
commit c32c55cb4d
46 changed files with 1426 additions and 0 deletions
+16
View File
@@ -0,0 +1,16 @@
# Node / pnpm
node_modules/
dist/
.pnpm-store/
# Build artefacts
*.js.map
*.d.ts.map
# Environment
.env
.env.local
.env.*.local
# OS
.DS_Store
+11
View File
@@ -0,0 +1,11 @@
# https://taskfile.dev
version: "3"
tasks:
environment:show:
desc: "Show environment vars"
cmds:
- |
./.scripts/base/api/environment-show.sh {{ .CLI_ARGS }}
silent: true
@@ -0,0 +1,9 @@
#!/bin/bash
# -------------------------------------------------------------------------------------
#
#
#
# -------------------------------------------------------------------------------------
. ./.scripts/base/lib/--index.sh
_base_environment_show "$@"
@@ -0,0 +1,28 @@
#!/bin/bash
# -------------------------------------------------------------------------------------
#
#
#
# -------------------------------------------------------------------------------------
export LOCAL_HOME_DIR=$(pwd)
export LOCAL_HOME_DIR_NAME=${PWD##*/}
export LOCAL_GIT_ROOT_DIR=$(dirname "${LOCAL_HOME_DIR}")
TMP_BASE_FILE_DOTENV=".env"
if
[ -e "${TMP_BASE_FILE_DOTENV}" ]
then
export $(
grep -v '^#' "${TMP_BASE_FILE_DOTENV}" | xargs
) >/dev/null 2>&1
fi
ALL_ARGS=("$@")
while [[ "$#" -gt 0 ]]; do
case $1 in
*) ;;
esac
shift
done
set -- "${ALL_ARGS[@]}"
@@ -0,0 +1,10 @@
#!/bin/bash
# -------------------------------------------------------------------------------------
#
#
#
# -------------------------------------------------------------------------------------
. ./.scripts/base/lib/--index-api.sh
# No required environment variables for skill-manager base
+12
View File
@@ -0,0 +1,12 @@
#!/bin/bash
# -------------------------------------------------------------------------------------
#
#
#
# -------------------------------------------------------------------------------------
. ./.scripts/base/lib/-ensure-environment-variable.sh
. ./.scripts/base/lib/-environment-show.sh
. ./.scripts/base/lib/-mask.sh
+20
View File
@@ -0,0 +1,20 @@
#!/bin/bash
# -------------------------------------------------------------------------------------
#
#
#
# -------------------------------------------------------------------------------------
. ./.scripts/loggers/lib/--index.sh
#
# --> passed parameters are read & exported environment variables
#
. ./.scripts/base/lib/--env-vars-reader.sh
#
# --> required environment variables are validated for existence
#
. ./.scripts/base/lib/--env-vars-validator.sh
#
# --> available functions are imported/exported
#
. ./.scripts/base/lib/--index-api.sh
# -------------------------------------------------------------------------------------
@@ -0,0 +1,18 @@
#!/bin/bash
# -------------------------------------------------------------------------------------
#
#
#
# -------------------------------------------------------------------------------------
. ./.scripts/loggers/lib/--index-api.sh
_base_ensureEnvironmentVariable() {
local FUNCTION_NAME="_base_ensureEnvironmentVariable"
local ENV_VAR_NAME="$1"
_loggers_info "${FUNCTION_NAME}" "ENV_VAR_NAME: ${ENV_VAR_NAME}"
if [ -z "${!ENV_VAR_NAME}" ]; then
_loggers_error "${FUNCTION_NAME}" "Missing required environment variable: ${ENV_VAR_NAME}. Check .env or .env-* files!"
exit 1
fi
}
@@ -0,0 +1,16 @@
#!/bin/bash
# -------------------------------------------------------------------------------------
#
#
#
# -------------------------------------------------------------------------------------
. ./.scripts/loggers/lib/--index-api.sh
_base_environment_show() {
local FUNCTION_NAME="_base_environment_show"
_loggers_info "${FUNCTION_NAME}" "LOCAL_HOME_DIR: ${LOCAL_HOME_DIR}"
_loggers_info "${FUNCTION_NAME}" "LOCAL_HOME_DIR_NAME: ${LOCAL_HOME_DIR_NAME}"
}
+19
View File
@@ -0,0 +1,19 @@
#!/bin/bash
# -------------------------------------------------------------------------------------
#
#
#
# -------------------------------------------------------------------------------------
_base_mask() {
local original_value="$1"
local RET_VAL
if
[[ -n "$original_value" && ${#original_value} -gt 2 ]]
then
RET_VAL="${original_value:0:1}$(printf '%*s' $((${#original_value} - 2)) '' | tr ' ' '*')${original_value: -1}"
else
RET_VAL="$(printf '%*s' ${#original_value} '' | tr ' ' '*')"
fi
echo "${RET_VAL}"
}
+29
View File
@@ -0,0 +1,29 @@
# https://taskfile.dev
version: "3"
tasks:
build:
desc: Compile TypeScript source to dist/
cmds:
- |
./.scripts/cli/api/build.sh {{ .CLI_ARGS }}
silent: true
next:
desc: |
Compute the next branch name (X.Y.Z.Q/{TASK_MANAGEMENT_CODE}-{FEATURE_X}-{SEQ}).
Usage: task cli:next -- --task-key="IIAA-123" --topic="api-cleanup" [--base=…] [--repo-dir=…]
cmds:
- |
./.scripts/cli/api/next.sh {{ .CLI_ARGS }}
silent: true
create:
desc: |
Compute and create the next branch locally (never pushes).
Usage: task cli:create -- --task-key="IIAA-123" --topic="api-cleanup" [--base=…] [--repo-dir=…]
cmds:
- |
./.scripts/cli/api/create.sh {{ .CLI_ARGS }}
silent: true
+11
View File
@@ -0,0 +1,11 @@
#!/bin/bash
# -------------------------------------------------------------------------------------
#
#
#
# -------------------------------------------------------------------------------------
set -e
. ./.scripts/cli/lib/--index.sh
_cli__build "$@"
+11
View File
@@ -0,0 +1,11 @@
#!/bin/bash
# -------------------------------------------------------------------------------------
#
#
#
# -------------------------------------------------------------------------------------
set -e
. ./.scripts/cli/lib/--index.sh
_cli__create "$@"
+11
View File
@@ -0,0 +1,11 @@
#!/bin/bash
# -------------------------------------------------------------------------------------
#
#
#
# -------------------------------------------------------------------------------------
set -e
. ./.scripts/cli/lib/--index.sh
_cli__next "$@"
@@ -0,0 +1,15 @@
#!/bin/bash
# -------------------------------------------------------------------------------------
#
#
#
# -------------------------------------------------------------------------------------
ALL_ARGS=("$@")
while [[ "$#" -gt 0 ]]; do
case $1 in
*) ;;
esac
shift
done
set -- "${ALL_ARGS[@]}"
@@ -0,0 +1,10 @@
#!/bin/bash
# -------------------------------------------------------------------------------------
#
#
#
# -------------------------------------------------------------------------------------
. ./.scripts/cli/lib/--index-api.sh
# No required environment variables for cli module
+12
View File
@@ -0,0 +1,12 @@
#!/bin/bash
# -------------------------------------------------------------------------------------
#
#
#
# -------------------------------------------------------------------------------------
. ./.scripts/cli/lib/-build.sh
. ./.scripts/cli/lib/-next.sh
. ./.scripts/cli/lib/-create.sh
+21
View File
@@ -0,0 +1,21 @@
#!/bin/bash
# -------------------------------------------------------------------------------------
#
#
#
# -------------------------------------------------------------------------------------
. ./.scripts/loggers/lib/--index.sh
. ./.scripts/base/lib/--index.sh
#
# --> passed parameters are read & exported environment variables
#
. ./.scripts/cli/lib/--env-vars-reader.sh
#
# --> required environment variables are validated for existence
#
. ./.scripts/cli/lib/--env-vars-validator.sh
#
# --> available functions are imported/exported
#
. ./.scripts/cli/lib/--index-api.sh
# -------------------------------------------------------------------------------------
+12
View File
@@ -0,0 +1,12 @@
#!/bin/bash
# -------------------------------------------------------------------------------------
#
#
#
# -------------------------------------------------------------------------------------
_cli__build() {
local FUNCTION_NAME="_cli__build"
_loggers_info "${FUNCTION_NAME}" "Compiling TypeScript source to dist/"
pnpm run build
}
+12
View File
@@ -0,0 +1,12 @@
#!/bin/bash
# -------------------------------------------------------------------------------------
#
#
#
# -------------------------------------------------------------------------------------
_cli__create() {
local FUNCTION_NAME="_cli__create"
_loggers_info "${FUNCTION_NAME}" "Creating branch via TypeScript CLI"
npx tsx src/cli/commands.ts --action create "$@"
}
+12
View File
@@ -0,0 +1,12 @@
#!/bin/bash
# -------------------------------------------------------------------------------------
#
#
#
# -------------------------------------------------------------------------------------
_cli__next() {
local FUNCTION_NAME="_cli__next"
_loggers_info "${FUNCTION_NAME}" "Computing next branch name via TypeScript CLI"
npx tsx src/cli/commands.ts --action next "$@"
}
@@ -0,0 +1,18 @@
#!/bin/bash
# -------------------------------------------------------------------------------------
#
# TOBE ADDED - Licence & Copyright Disclaimer
#
# -------------------------------------------------------------------------------------
export LOGGER_TRAILING_NEW_LINE_ENABLED="TRUE"
export LOGGER_IS_ENABLED_ERROR=true
export LOGGER_IS_ENABLED_INFO=true
export LOGGER_IS_ENABLED_WARN=true
export LOGGER_IS_ENABLED_DEBUG=true
export LOGGER_IS_ENABLED_TRACE=false
export LOCAL_STRING_005_SPACES=" "
export LOCAL_STRING_010_SPACES="${LOCAL_STRING_005_SPACES}${LOCAL_STRING_005_SPACES}"
export LOCAL_STRING_050_SPACES="${LOCAL_STRING_010_SPACES}${LOCAL_STRING_010_SPACES}${LOCAL_STRING_010_SPACES}${LOCAL_STRING_010_SPACES}${LOCAL_STRING_010_SPACES}"
export LOCAL_STRING_070_SPACES="${LOCAL_STRING_050_SPACES}${LOCAL_STRING_010_SPACES}${LOCAL_STRING_010_SPACES}"
+9
View File
@@ -0,0 +1,9 @@
#!/bin/bash
# -------------------------------------------------------------------------------------
#
# TOBE ADDED - Licence & Copyright Disclaimer
#
# -------------------------------------------------------------------------------------
. ./.scripts/loggers/lib/--env-vars-reader.sh
. ./.scripts/loggers/lib/--index.sh
@@ -0,0 +1,22 @@
#!/bin/bash
# -------------------------------------------------------------------------------------
#
# TOBE ADDED - Licence & Copyright Disclaimer
#
# -------------------------------------------------------------------------------------
. ./.scripts/loggers/lib/-enable-trailing-new-line.sh
. ./.scripts/loggers/lib/-info.sh
. ./.scripts/loggers/lib/-trace.sh
. ./.scripts/loggers/lib/-debug.sh
. ./.scripts/loggers/lib/-error.sh
. ./.scripts/loggers/lib/-warn.sh
. ./.scripts/loggers/lib/-printf--debug.sh
. ./.scripts/loggers/lib/-printf--info.sh
. ./.scripts/loggers/lib/-printf--trace.sh
. ./.scripts/loggers/lib/-printf--warn.sh
. ./.scripts/loggers/lib/-empty-line.sh
. ./.scripts/loggers/lib/-waiting-dot.sh
+15
View File
@@ -0,0 +1,15 @@
#!/bin/bash
# -------------------------------------------------------------------------------------
#
# TOBE ADDED - Licence & Copyright Disclaimer
#
# -------------------------------------------------------------------------------------
#
# --> passed parameters are read & exported environment variables
#
. ./.scripts/loggers/lib/--env-vars-reader.sh
#
# --> available functions are imported/exported
#
. ./.scripts/loggers/lib/--index-api.sh
# -------------------------------------------------------------------------------------
+20
View File
@@ -0,0 +1,20 @@
#!/bin/bash
# -------------------------------------------------------------------------------------
#
# TOBE ADDED - Licence & Copyright Disclaimer
#
# -------------------------------------------------------------------------------------
. ./.scripts/loggers/lib/-enable-trailing-new-line.sh
_loggers_debug() {
_loggers_enableLoggerTrailingNewLine
if
[ "${LOGGER_IS_ENABLED_DEBUG}" = true ]
then
local TMP_1ST_PARAM="${1}${LOCAL_STRING_070_SPACES}"
TMP_1ST_PARAM="${TMP_1ST_PARAM:0:70}"
local TMP_LINE="# DEBUG # ${TMP_1ST_PARAM} # ${2}"
echo -e "\033[1;36m${TMP_LINE}\033[0m" >&2
fi
}
@@ -0,0 +1,13 @@
#!/bin/bash
# -------------------------------------------------------------------------------------
#
# TOBE ADDED - Licence & Copyright Disclaimer
#
# -------------------------------------------------------------------------------------
. ./.scripts/loggers/lib/-enable-trailing-new-line.sh
_loggers_emptyLine() {
_loggers_enableLoggerTrailingNewLine
echo -e "" >&2
}
@@ -0,0 +1,15 @@
#!/bin/bash
# -------------------------------------------------------------------------------------
#
# TOBE ADDED - Licence & Copyright Disclaimer
#
# -------------------------------------------------------------------------------------
_loggers_enableLoggerTrailingNewLine() {
if
[ -z "$LOGGER_TRAILING_NEW_LINE_DISABLED" ]
then
echo "" >&2
export LOGGER_TRAILING_NEW_LINE_DISABLED="TRUE"
fi
}
+21
View File
@@ -0,0 +1,21 @@
#!/bin/bash
# -------------------------------------------------------------------------------------
#
# TOBE ADDED - Licence & Copyright Disclaimer
#
# -------------------------------------------------------------------------------------
. ./.scripts/loggers/lib/-enable-trailing-new-line.sh
_loggers_error() {
_loggers_enableLoggerTrailingNewLine
if
[ "${LOGGER_IS_ENABLED_ERROR}" = true ]
then
local TMP_1ST_PARAM="${1}${LOCAL_STRING_070_SPACES}"
TMP_1ST_PARAM="${TMP_1ST_PARAM:0:70}"
local TMP_LINE="# ERROR # ${TMP_1ST_PARAM} # $2"
echo -e "\033[1;31m${TMP_LINE}\033[0m" >&2
fi
}
+20
View File
@@ -0,0 +1,20 @@
#!/bin/bash
# -------------------------------------------------------------------------------------
#
# TOBE ADDED - Licence & Copyright Disclaimer
#
# -------------------------------------------------------------------------------------
. ./.scripts/loggers/lib/-enable-trailing-new-line.sh
_loggers_info() {
_loggers_enableLoggerTrailingNewLine
if
[ "${LOGGER_IS_ENABLED_INFO}" = true ]
then
local TMP_1ST_PARAM="${1}${LOCAL_STRING_070_SPACES}"
TMP_1ST_PARAM="${TMP_1ST_PARAM:0:70}"
local TMP_LINE="# INFO # ${TMP_1ST_PARAM} # ${2}"
echo -e "${TMP_LINE}" >&2
fi
}
@@ -0,0 +1,14 @@
#!/bin/bash
# -------------------------------------------------------------------------------------
#
#
#
# -------------------------------------------------------------------------------------
_loggers_printf__debug() {
if
[ "${LOGGER_IS_ENABLED_DEBUG}" = true ]
then
printf '\033[1;36m%s\033[0m\n' "$*" >&2
fi
}
@@ -0,0 +1,14 @@
#!/bin/bash
# -------------------------------------------------------------------------------------
#
#
#
# -------------------------------------------------------------------------------------
_loggers_printf__info() {
if
[ "${LOGGER_IS_ENABLED_INFO}" = true ]
then
printf '\033[1;32m%s\033[0m\n' "$*" >&2
fi
}
@@ -0,0 +1,14 @@
#!/bin/bash
# -------------------------------------------------------------------------------------
#
#
#
# -------------------------------------------------------------------------------------
_loggers_printf__trace() {
if
[ "${LOGGER_IS_ENABLED_TRACE}" = true ]
then
printf '\033[0;90m%s\033[0m\n' "$*" >&2
fi
}
@@ -0,0 +1,14 @@
#!/bin/bash
# -------------------------------------------------------------------------------------
#
#
#
# -------------------------------------------------------------------------------------
_loggers_printf__warn() {
if
[ "${LOGGER_IS_ENABLED_WARN}" = true ]
then
printf '\033[1;33m%s\033[0m\n' "$*" >&2
fi
}
+21
View File
@@ -0,0 +1,21 @@
#!/bin/bash
# -------------------------------------------------------------------------------------
#
# TOBE ADDED - Licence & Copyright Disclaimer
#
# -------------------------------------------------------------------------------------
. ./.scripts/loggers/lib/-enable-trailing-new-line.sh
_loggers_trace() {
_loggers_enableLoggerTrailingNewLine
if
[ "${LOGGER_IS_ENABLED_TRACE}" = true ]
then
local TMP_1ST_PARAM="${1}${LOCAL_STRING_070_SPACES}"
TMP_1ST_PARAM="${TMP_1ST_PARAM:0:70}"
local TMP_LINE="# TRACE # ${TMP_1ST_PARAM} # $2"
echo -e "\033[0;94m${TMP_LINE}\033[0m" >&2
fi
}
@@ -0,0 +1,13 @@
#!/bin/bash
# -------------------------------------------------------------------------------------
#
# TOBE ADDED - Licence & Copyright Disclaimer
#
# -------------------------------------------------------------------------------------
. ./.scripts/loggers/lib/-enable-trailing-new-line.sh
_loggers_waitingDot() {
echo -n "." >&2
export LOGGER_TRAILING_NEW_LINE_DISABLED=""
}
+20
View File
@@ -0,0 +1,20 @@
#!/bin/bash
# -------------------------------------------------------------------------------------
#
# TOBE ADDED - Licence & Copyright Disclaimer
#
# -------------------------------------------------------------------------------------
. ./.scripts/loggers/lib/-enable-trailing-new-line.sh
_loggers_warn() {
_loggers_enableLoggerTrailingNewLine
if
[ "${LOGGER_IS_ENABLED_WARN}" = true ]
then
local TMP_1ST_PARAM="${1}${LOCAL_STRING_070_SPACES}"
TMP_1ST_PARAM="${TMP_1ST_PARAM:0:70}"
local TMP_LINE="# WARN # ${TMP_1ST_PARAM} # $2"
echo -e "\033[1;33m${TMP_LINE}\033[0m" >&2
fi
}
+39
View File
@@ -0,0 +1,39 @@
# https://taskfile.dev
version: "3"
includes:
base: ./.scripts/base/Taskfile.yml
cli: ./.scripts/cli/Taskfile.yml
tasks:
default:
cmds:
- task --list-all
silent: true
build:
desc: Compile TypeScript source to dist/
cmds:
- task: cli:build
silent: true
next:
desc: |
Compute the next branch name (X.Y.Z.Q/{TASK_MANAGEMENT_CODE}-{FEATURE_X}-{SEQ}).
Usage: task next -- --task-key="IIAA-123" --topic="api-cleanup" [--base=…] [--repo-dir=…]
cmds:
- task: cli:next
vars:
CLI_ARGS: "{{ .CLI_ARGS }}"
silent: true
create:
desc: |
Compute and create the next branch locally (never pushes).
Usage: task create -- --task-key="IIAA-123" --topic="api-cleanup" [--base=…] [--repo-dir=…]
cmds:
- task: cli:create
vars:
CLI_ARGS: "{{ .CLI_ARGS }}"
silent: true
+23
View File
@@ -0,0 +1,23 @@
{
"name": "branch-tools",
"version": "0.1.0",
"description": "CLI tools for version-aware git branch naming (branching-strategy skill)",
"type": "module",
"packageManager": "pnpm@9.15.0",
"bin": {
"branch-tools": "dist/cli/commands.js"
},
"scripts": {
"build": "tsc",
"cli": "tsx src/cli/commands.ts"
},
"devDependencies": {
"@types/js-yaml": "^4.0.9",
"@types/node": "^22.0.0",
"tsx": "^4.19.0",
"typescript": "^5.7.0"
},
"dependencies": {
"js-yaml": "^4.1.0"
}
}
+354
View File
@@ -0,0 +1,354 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
dependencies:
js-yaml:
specifier: ^4.1.0
version: 4.3.0
devDependencies:
'@types/js-yaml':
specifier: ^4.0.9
version: 4.0.9
'@types/node':
specifier: ^22.0.0
version: 22.20.1
tsx:
specifier: ^4.19.0
version: 4.23.1
typescript:
specifier: ^5.7.0
version: 5.9.3
packages:
'@esbuild/aix-ppc64@0.28.1':
resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [aix]
'@esbuild/android-arm64@0.28.1':
resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [android]
'@esbuild/android-arm@0.28.1':
resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==}
engines: {node: '>=18'}
cpu: [arm]
os: [android]
'@esbuild/android-x64@0.28.1':
resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==}
engines: {node: '>=18'}
cpu: [x64]
os: [android]
'@esbuild/darwin-arm64@0.28.1':
resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==}
engines: {node: '>=18'}
cpu: [arm64]
os: [darwin]
'@esbuild/darwin-x64@0.28.1':
resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [darwin]
'@esbuild/freebsd-arm64@0.28.1':
resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==}
engines: {node: '>=18'}
cpu: [arm64]
os: [freebsd]
'@esbuild/freebsd-x64@0.28.1':
resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [freebsd]
'@esbuild/linux-arm64@0.28.1':
resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==}
engines: {node: '>=18'}
cpu: [arm64]
os: [linux]
'@esbuild/linux-arm@0.28.1':
resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==}
engines: {node: '>=18'}
cpu: [arm]
os: [linux]
'@esbuild/linux-ia32@0.28.1':
resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==}
engines: {node: '>=18'}
cpu: [ia32]
os: [linux]
'@esbuild/linux-loong64@0.28.1':
resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==}
engines: {node: '>=18'}
cpu: [loong64]
os: [linux]
'@esbuild/linux-mips64el@0.28.1':
resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==}
engines: {node: '>=18'}
cpu: [mips64el]
os: [linux]
'@esbuild/linux-ppc64@0.28.1':
resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [linux]
'@esbuild/linux-riscv64@0.28.1':
resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==}
engines: {node: '>=18'}
cpu: [riscv64]
os: [linux]
'@esbuild/linux-s390x@0.28.1':
resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==}
engines: {node: '>=18'}
cpu: [s390x]
os: [linux]
'@esbuild/linux-x64@0.28.1':
resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==}
engines: {node: '>=18'}
cpu: [x64]
os: [linux]
'@esbuild/netbsd-arm64@0.28.1':
resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==}
engines: {node: '>=18'}
cpu: [arm64]
os: [netbsd]
'@esbuild/netbsd-x64@0.28.1':
resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==}
engines: {node: '>=18'}
cpu: [x64]
os: [netbsd]
'@esbuild/openbsd-arm64@0.28.1':
resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openbsd]
'@esbuild/openbsd-x64@0.28.1':
resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==}
engines: {node: '>=18'}
cpu: [x64]
os: [openbsd]
'@esbuild/openharmony-arm64@0.28.1':
resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openharmony]
'@esbuild/sunos-x64@0.28.1':
resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [sunos]
'@esbuild/win32-arm64@0.28.1':
resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==}
engines: {node: '>=18'}
cpu: [arm64]
os: [win32]
'@esbuild/win32-ia32@0.28.1':
resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==}
engines: {node: '>=18'}
cpu: [ia32]
os: [win32]
'@esbuild/win32-x64@0.28.1':
resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==}
engines: {node: '>=18'}
cpu: [x64]
os: [win32]
'@types/js-yaml@4.0.9':
resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==}
'@types/node@22.20.1':
resolution: {integrity: sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==}
argparse@2.0.1:
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
esbuild@0.28.1:
resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==}
engines: {node: '>=18'}
hasBin: true
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
js-yaml@4.3.0:
resolution: {integrity: sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==}
hasBin: true
tsx@4.23.1:
resolution: {integrity: sha512-GQHnkIfxyx1wYCOS/wonik5MVRZU9hi1TEZmzGZSCJB1y9YgoZ8H6itNE/u4suE+yLmOzuE4E5S4TZ/ZX2wcWQ==}
engines: {node: '>=18.0.0'}
hasBin: true
typescript@5.9.3:
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
engines: {node: '>=14.17'}
hasBin: true
undici-types@6.21.0:
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
snapshots:
'@esbuild/aix-ppc64@0.28.1':
optional: true
'@esbuild/android-arm64@0.28.1':
optional: true
'@esbuild/android-arm@0.28.1':
optional: true
'@esbuild/android-x64@0.28.1':
optional: true
'@esbuild/darwin-arm64@0.28.1':
optional: true
'@esbuild/darwin-x64@0.28.1':
optional: true
'@esbuild/freebsd-arm64@0.28.1':
optional: true
'@esbuild/freebsd-x64@0.28.1':
optional: true
'@esbuild/linux-arm64@0.28.1':
optional: true
'@esbuild/linux-arm@0.28.1':
optional: true
'@esbuild/linux-ia32@0.28.1':
optional: true
'@esbuild/linux-loong64@0.28.1':
optional: true
'@esbuild/linux-mips64el@0.28.1':
optional: true
'@esbuild/linux-ppc64@0.28.1':
optional: true
'@esbuild/linux-riscv64@0.28.1':
optional: true
'@esbuild/linux-s390x@0.28.1':
optional: true
'@esbuild/linux-x64@0.28.1':
optional: true
'@esbuild/netbsd-arm64@0.28.1':
optional: true
'@esbuild/netbsd-x64@0.28.1':
optional: true
'@esbuild/openbsd-arm64@0.28.1':
optional: true
'@esbuild/openbsd-x64@0.28.1':
optional: true
'@esbuild/openharmony-arm64@0.28.1':
optional: true
'@esbuild/sunos-x64@0.28.1':
optional: true
'@esbuild/win32-arm64@0.28.1':
optional: true
'@esbuild/win32-ia32@0.28.1':
optional: true
'@esbuild/win32-x64@0.28.1':
optional: true
'@types/js-yaml@4.0.9': {}
'@types/node@22.20.1':
dependencies:
undici-types: 6.21.0
argparse@2.0.1: {}
esbuild@0.28.1:
optionalDependencies:
'@esbuild/aix-ppc64': 0.28.1
'@esbuild/android-arm': 0.28.1
'@esbuild/android-arm64': 0.28.1
'@esbuild/android-x64': 0.28.1
'@esbuild/darwin-arm64': 0.28.1
'@esbuild/darwin-x64': 0.28.1
'@esbuild/freebsd-arm64': 0.28.1
'@esbuild/freebsd-x64': 0.28.1
'@esbuild/linux-arm': 0.28.1
'@esbuild/linux-arm64': 0.28.1
'@esbuild/linux-ia32': 0.28.1
'@esbuild/linux-loong64': 0.28.1
'@esbuild/linux-mips64el': 0.28.1
'@esbuild/linux-ppc64': 0.28.1
'@esbuild/linux-riscv64': 0.28.1
'@esbuild/linux-s390x': 0.28.1
'@esbuild/linux-x64': 0.28.1
'@esbuild/netbsd-arm64': 0.28.1
'@esbuild/netbsd-x64': 0.28.1
'@esbuild/openbsd-arm64': 0.28.1
'@esbuild/openbsd-x64': 0.28.1
'@esbuild/openharmony-arm64': 0.28.1
'@esbuild/sunos-x64': 0.28.1
'@esbuild/win32-arm64': 0.28.1
'@esbuild/win32-ia32': 0.28.1
'@esbuild/win32-x64': 0.28.1
fsevents@2.3.3:
optional: true
js-yaml@4.3.0:
dependencies:
argparse: 2.0.1
tsx@4.23.1:
dependencies:
esbuild: 0.28.1
optionalDependencies:
fsevents: 2.3.3
typescript@5.9.3: {}
undici-types@6.21.0: {}
+62
View File
@@ -0,0 +1,62 @@
/**
* Action: create
*
* Computes the next branch name (same rules as the `next` action) and creates
* the branch from the base branch:
*
* origin/test → origin/main → error (the caller must ask the user and pass --base)
*
* The caller MUST have confirmed the branch name with the user first (run `next`,
* show the name, get confirmation). Never pushes — publishing stays a human decision.
*
* Usage:
* branch-tools --action create --task-key IIAA-123 --topic api-cleanup [--base origin/develop] [--repo-dir /path]
*/
import { resolve } from "node:path";
import { computeBranchName, detectBaseBranch, git } from "../next/action.js";
export function run(options: Record<string, string | undefined>): Record<string, unknown> {
const { branch, error, parts } = computeBranchName(options);
if (error) return { action: "create", success: false, error };
const repoDir = resolve(options["repo-dir"] ?? process.cwd());
const base = options["base"] ?? detectBaseBranch(repoDir);
if (!base) {
return {
action: "create",
success: false,
branch,
error: "Remote has neither origin/test nor origin/main — ask the user which base branch to use and pass it via --base",
};
}
try {
git(repoDir, ["fetch", "--prune"]);
} catch {
// no remote or offline — branch from the local ref instead
}
try {
git(repoDir, ["checkout", "-b", branch!, base]);
} catch (err) {
return {
action: "create",
success: false,
branch,
base,
error: err instanceof Error ? err.message : String(err),
};
}
return {
action: "create",
success: true,
branch,
base,
...parts ? { parts } : {},
"repo-dir": repoDir,
note: "Branch created locally — push and PR into test are the user's decision",
};
}
+121
View File
@@ -0,0 +1,121 @@
/**
* Action: next
*
* Computes the next branch name for any git repository following the convention:
*
* X.Y.Z.Q/{TASK_MANAGEMENT_CODE}-{FEATURE_X}-{SEQ}
*
* - X.Y.Z.Q — version, read from git tags ONLY (latest semver tag;
* 3-part tags get Q=0). No tags → hard error.
* - TASK_MANAGEMENT_CODE — task/ticket identifier, e.g. IIAA-123 (--task-key)
* - FEATURE_X — kebab-case feature/focus topic (--topic)
* - SEQ — zero-padded 3-digit sequence, auto-incremented by scanning
* existing local + remote branches with the same prefix
*
* Repository-independent: operates on --repo-dir (default: current directory).
*
* Usage:
* branch-tools --action next --task-key IIAA-123 --topic api-cleanup [--repo-dir /path]
*/
import { execFileSync } from "node:child_process";
import { resolve } from "node:path";
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
const TAG_PATTERN = /^v?(\d+)\.(\d+)\.(\d+)(?:\.(\d+))?$/;
export function git(repoDir: string, args: string[]): string {
return execFileSync("git", ["-C", repoDir, ...args], { encoding: "utf-8" }).trim();
}
export function detectVersion(repoDir: string): string | undefined {
const tags = git(repoDir, ["tag", "--list", "--sort=-v:refname"])
.split("\n")
.map((t) => t.trim())
.filter((t) => TAG_PATTERN.test(t));
if (tags.length === 0) return undefined;
const m = tags[0].match(TAG_PATTERN)!;
return `${m[1]}.${m[2]}.${m[3]}.${m[4] ?? "0"}`;
}
export function detectBaseBranch(repoDir: string): string | undefined {
for (const candidate of ["origin/test", "origin/main"]) {
try {
git(repoDir, ["show-ref", "--verify", "--quiet", `refs/remotes/${candidate}`]);
return candidate;
} catch {
// not present — try next
}
}
return undefined;
}
export function nextSequence(repoDir: string, prefix: string): string {
const branches = git(repoDir, ["branch", "-a", "--format=%(refname:short)"])
.split("\n")
.map((b) => b.trim().replace(/^origin\//, ""));
let max = 0;
for (const branch of branches) {
if (!branch.startsWith(`${prefix}-`)) continue;
const seq = branch.slice(prefix.length + 1);
if (/^\d+$/.test(seq)) max = Math.max(max, parseInt(seq, 10));
}
return String(max + 1).padStart(3, "0");
}
export function computeBranchName(
options: Record<string, string | undefined>
): { branch?: string; error?: string; parts?: Record<string, string> } {
const taskKey = options["task-key"];
if (!taskKey) return { error: "Missing required option: --task-key (e.g. IIAA-123)" };
if (!/^[A-Z][A-Z0-9]*-[A-Za-z0-9]+$/.test(taskKey)) {
return { error: `'task-key' must look like PROJECT-ID (e.g. IIAA-123). Got: '${taskKey}'` };
}
const topic = options["topic"];
if (!topic) return { error: "Missing required option: --topic (kebab-case feature topic)" };
if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(topic)) {
return { error: `'topic' must be kebab-case (a-z, 0-9, single hyphens). Got: '${topic}'` };
}
const repoDir = resolve(options["repo-dir"] ?? process.cwd());
try {
git(repoDir, ["rev-parse", "--git-dir"]);
} catch {
return { error: `Not a git repository: ${repoDir}` };
}
const version = detectVersion(repoDir);
if (!version) {
return { error: `No version tag found in ${repoDir} — the version (X.Y.Z.Q) is read from git tags only. Create one first, e.g.: git tag 4.0.0.0` };
}
const prefix = `${version}/${taskKey}-${topic}`;
const seq = nextSequence(repoDir, prefix);
return {
branch: `${prefix}-${seq}`,
parts: { version, "task-key": taskKey, topic, seq },
};
}
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
export function run(options: Record<string, string | undefined>): Record<string, unknown> {
const { branch, error, parts } = computeBranchName(options);
if (error) return { action: "next", success: false, error };
const repoDir = resolve(options["repo-dir"] ?? process.cwd());
const base = detectBaseBranch(repoDir);
return {
action: "next",
success: true,
branch,
"base-branch": base ?? "NONE — remote has neither test nor main; ask the user",
...parts ? { parts } : {},
};
}
+126
View File
@@ -0,0 +1,126 @@
#!/usr/bin/env node
/**
* branch-tools CLI dispatcher
*
* Usage:
* branch-tools --action <action-name> [options]
*
* Actions:
* next — compute the next branch name (version from git tags only)
* create — compute and create the branch locally
*
* Output is always YAML to stdout. Exit 0 on success, 1 on error.
*/
import { parseArgs } from "node:util";
import * as yaml from "js-yaml";
// ---------------------------------------------------------------------------
// Action registry
// ---------------------------------------------------------------------------
type ActionModule = {
run: (options: Record<string, string | undefined>) => Record<string, unknown>;
};
const ACTIONS: Record<string, () => Promise<ActionModule>> = {
next: () => import("../actions/next/action.js"),
create: () => import("../actions/create/action.js"),
};
// ---------------------------------------------------------------------------
// CLI
// ---------------------------------------------------------------------------
async function main(): Promise<void> {
let values: {
action?: string;
"task-key"?: string;
topic?: string;
base?: string;
"repo-dir"?: string;
help?: boolean;
};
try {
({ values } = parseArgs({
args: process.argv.slice(2),
options: {
action: { type: "string", short: "a" },
"task-key": { type: "string" },
topic: { type: "string" },
base: { type: "string" },
"repo-dir": { type: "string" },
help: { type: "boolean", short: "h" },
},
strict: true,
}));
} catch (err) {
printError(String(err));
process.exit(1);
}
if (values.help || !values.action) {
console.log(`branch-tools --action <action> [options]
Branch convention: X.Y.Z.Q/{TASK_MANAGEMENT_CODE}-{FEATURE_X}-{SEQ}
Version (X.Y.Z.Q) is read from git tags ONLY. Branches are cut from origin/test (fallback origin/main).
Actions:
next Compute the next branch name
create Compute and create the branch locally (never pushes)
Options (both actions):
--task-key <key> Task identifier, e.g. IIAA-123 (required)
--topic <topic> Kebab-case focus topic, e.g. api-cleanup (required)
--base <branch> Base branch for create (default: origin/test, then origin/main)
--repo-dir <path> Target repository (default: current directory)
Examples:
branch-tools --action next --task-key IIAA-123 --topic api-cleanup
branch-tools --action create --task-key IIAA-123 --topic api-cleanup --repo-dir /path/to/repo
`);
process.exit(values.help ? 0 : 1);
}
const actionName = values.action;
if (!ACTIONS[actionName]) {
printError(
`Unknown action '${actionName}'. Available: ${Object.keys(ACTIONS).join(", ")}`
);
process.exit(1);
}
try {
const mod = await ACTIONS[actionName]();
const result = mod.run({
"task-key": values["task-key"],
topic: values.topic,
base: values.base,
"repo-dir": values["repo-dir"],
});
process.stdout.write(
yaml.dump(result, { noRefs: true, sortKeys: false, lineWidth: 120 })
);
const success = (result["success"] as boolean | undefined) ?? true;
process.exit(success ? 0 : 1);
} catch (err) {
const errorOutput = {
success: false,
action: actionName,
error: err instanceof Error ? err.message : String(err),
};
process.stdout.write(yaml.dump(errorOutput, { noRefs: true, sortKeys: false }));
process.exit(1);
}
}
function printError(msg: string): void {
process.stderr.write(`Error: ${msg}\n`);
process.stderr.write(
`Usage: branch-tools --action <${Object.keys(ACTIONS).join("|")}> [options]\n`
);
}
main();
+17
View File
@@ -0,0 +1,17 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "dist",
"rootDir": "src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}