From c32c55cb4db8242a6d204b5d9aa02fbc5c2fe795 Mon Sep 17 00:00:00 2001 From: Oleg Lukasonok Date: Fri, 24 Jul 2026 13:17:02 +0300 Subject: [PATCH] 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 --- README.md | 2 + SKILL.md | 104 +++++ scripts/.gitignore | 16 + scripts/.scripts/base/Taskfile.yml | 11 + scripts/.scripts/base/api/environment-show.sh | 9 + .../.scripts/base/lib/--env-vars-reader.sh | 28 ++ .../.scripts/base/lib/--env-vars-validator.sh | 10 + scripts/.scripts/base/lib/--index-api.sh | 12 + scripts/.scripts/base/lib/--index.sh | 20 + .../base/lib/-ensure-environment-variable.sh | 18 + .../.scripts/base/lib/-environment-show.sh | 16 + scripts/.scripts/base/lib/-mask.sh | 19 + scripts/.scripts/cli/Taskfile.yml | 29 ++ scripts/.scripts/cli/api/build.sh | 11 + scripts/.scripts/cli/api/create.sh | 11 + scripts/.scripts/cli/api/next.sh | 11 + scripts/.scripts/cli/lib/--env-vars-reader.sh | 15 + .../.scripts/cli/lib/--env-vars-validator.sh | 10 + scripts/.scripts/cli/lib/--index-api.sh | 12 + scripts/.scripts/cli/lib/--index.sh | 21 ++ scripts/.scripts/cli/lib/-build.sh | 12 + scripts/.scripts/cli/lib/-create.sh | 12 + scripts/.scripts/cli/lib/-next.sh | 12 + .../.scripts/loggers/lib/--env-vars-reader.sh | 18 + scripts/.scripts/loggers/lib/--import.sh | 9 + scripts/.scripts/loggers/lib/--index-api.sh | 22 ++ scripts/.scripts/loggers/lib/--index.sh | 15 + scripts/.scripts/loggers/lib/-debug.sh | 20 + scripts/.scripts/loggers/lib/-empty-line.sh | 13 + .../loggers/lib/-enable-trailing-new-line.sh | 15 + scripts/.scripts/loggers/lib/-error.sh | 21 ++ scripts/.scripts/loggers/lib/-info.sh | 20 + .../.scripts/loggers/lib/-printf--debug.sh | 14 + scripts/.scripts/loggers/lib/-printf--info.sh | 14 + .../.scripts/loggers/lib/-printf--trace.sh | 14 + scripts/.scripts/loggers/lib/-printf--warn.sh | 14 + scripts/.scripts/loggers/lib/-trace.sh | 21 ++ scripts/.scripts/loggers/lib/-waiting-dot.sh | 13 + scripts/.scripts/loggers/lib/-warn.sh | 20 + scripts/Taskfile.yml | 39 ++ scripts/package.json | 23 ++ scripts/pnpm-lock.yaml | 354 ++++++++++++++++++ scripts/src/actions/create/action.ts | 62 +++ scripts/src/actions/next/action.ts | 121 ++++++ scripts/src/cli/commands.ts | 126 +++++++ scripts/tsconfig.json | 17 + 46 files changed, 1426 insertions(+) create mode 100644 SKILL.md create mode 100644 scripts/.gitignore create mode 100644 scripts/.scripts/base/Taskfile.yml create mode 100644 scripts/.scripts/base/api/environment-show.sh create mode 100644 scripts/.scripts/base/lib/--env-vars-reader.sh create mode 100644 scripts/.scripts/base/lib/--env-vars-validator.sh create mode 100644 scripts/.scripts/base/lib/--index-api.sh create mode 100644 scripts/.scripts/base/lib/--index.sh create mode 100644 scripts/.scripts/base/lib/-ensure-environment-variable.sh create mode 100644 scripts/.scripts/base/lib/-environment-show.sh create mode 100644 scripts/.scripts/base/lib/-mask.sh create mode 100644 scripts/.scripts/cli/Taskfile.yml create mode 100755 scripts/.scripts/cli/api/build.sh create mode 100755 scripts/.scripts/cli/api/create.sh create mode 100755 scripts/.scripts/cli/api/next.sh create mode 100644 scripts/.scripts/cli/lib/--env-vars-reader.sh create mode 100644 scripts/.scripts/cli/lib/--env-vars-validator.sh create mode 100644 scripts/.scripts/cli/lib/--index-api.sh create mode 100644 scripts/.scripts/cli/lib/--index.sh create mode 100644 scripts/.scripts/cli/lib/-build.sh create mode 100644 scripts/.scripts/cli/lib/-create.sh create mode 100644 scripts/.scripts/cli/lib/-next.sh create mode 100644 scripts/.scripts/loggers/lib/--env-vars-reader.sh create mode 100644 scripts/.scripts/loggers/lib/--import.sh create mode 100644 scripts/.scripts/loggers/lib/--index-api.sh create mode 100644 scripts/.scripts/loggers/lib/--index.sh create mode 100644 scripts/.scripts/loggers/lib/-debug.sh create mode 100644 scripts/.scripts/loggers/lib/-empty-line.sh create mode 100644 scripts/.scripts/loggers/lib/-enable-trailing-new-line.sh create mode 100644 scripts/.scripts/loggers/lib/-error.sh create mode 100644 scripts/.scripts/loggers/lib/-info.sh create mode 100644 scripts/.scripts/loggers/lib/-printf--debug.sh create mode 100644 scripts/.scripts/loggers/lib/-printf--info.sh create mode 100644 scripts/.scripts/loggers/lib/-printf--trace.sh create mode 100644 scripts/.scripts/loggers/lib/-printf--warn.sh create mode 100644 scripts/.scripts/loggers/lib/-trace.sh create mode 100644 scripts/.scripts/loggers/lib/-waiting-dot.sh create mode 100644 scripts/.scripts/loggers/lib/-warn.sh create mode 100644 scripts/Taskfile.yml create mode 100644 scripts/package.json create mode 100644 scripts/pnpm-lock.yaml create mode 100644 scripts/src/actions/create/action.ts create mode 100644 scripts/src/actions/next/action.ts create mode 100644 scripts/src/cli/commands.ts create mode 100644 scripts/tsconfig.json diff --git a/README.md b/README.md index 462cfb9..e8c5b1e 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,4 @@ # branching-strategy Git branching-strategy Agent Skill — version-aware branch naming, branches cut from origin/test, PRs into test. TypeScript CLI: next + create. + +See [SKILL.md](SKILL.md). diff --git a/SKILL.md b/SKILL.md new file mode 100644 index 0000000..a52f6ee --- /dev/null +++ b/SKILL.md @@ -0,0 +1,104 @@ +--- +name: branching-strategy +description: > + Principles and tooling for git branching in any repository — version-aware branch + naming, branch creation from origin/test, and the PR-into-test merge rule. Use this + skill whenever a new git branch must be named or created, or when committing, pushing, + or merging in any repository — even if the user only says "new branch", "commit this", + "push it", or "merge". Enforces the convention + X.Y.Z.Q/{TASK_MANAGEMENT_CODE}-{FEATURE_X}-{SEQ} with the version read from git tags + only. Also use when deciding what an agent may do autonomously in a repository versus + what requires explicit human approval. +license: Proprietary +metadata: + author: workspace-swiss-knife + version: "2.0" + spec: agentskills.io/specification + origin-repository: git@github.ibm.com:CTOTools-skills-code-agent/branching-strategy.git + origin-path: $HOME/projects-ibm/cognitive-architect/workspace-skills-code-agent/branching-strategy +--- + +# Branching Strategy + +Git branching principles and tooling, for any repository. + +## Main rules + +### Rule 1 — All local work happens on a branch created from `origin/test` + +- Working locally with **any** repository always requires a branch — never commit on + the base branch itself. +- The branch is created from **`origin/test`**; if the remote has no `test`, fall back + to **`origin/main`**; if the remote has neither, **ask the user** which base to use — + never guess. +- Fetch first (`git fetch --prune`) so the branch is cut from the current remote state. +- **Before creating the branch, present the computed branch name to the user and get + their confirmation** — run the `next` action, show the name, create only after a yes. + +### Rule 2 — Branch naming convention + +``` +X.Y.Z.Q/{TASK_MANAGEMENT_CODE}-{FEATURE_X}-{SEQ} +``` + +Example: `4.0.0.0/IIAA-123-api-cleanup-001` + +| Segment | Meaning | Source | +|---|---|---| +| `X.Y.Z.Q` | Version | **Git tags only** — latest semver tag of the target repo (`v` prefix allowed; 3-part tags get `Q=0`). No tags → stop and ask which version to tag, never guess. | +| `TASK_MANAGEMENT_CODE` | Task/ticket identifier, e.g. `IIAA-123` | Provided per task (`--task-key`) | +| `FEATURE_X` | Kebab-case feature/focus topic | Provided per task (`--topic`) | +| `SEQ` | 3-digit sequence, `001`-based | Auto-incremented by scanning existing local + remote branches with the same prefix | + +The version is **never** taken from `package.json`, `VERSION` files, or guessed. + +### Rule 3 — Merging into `test` is via Pull Request, by the user, always + +- Every branch is merged into **`test`** through a **Pull Request** — never by direct + merge + push. +- **Merging always happens manually by the user.** The agent may prepare and push the + branch and hand over the PR compare URL; it never creates the merge. + +## Supporting principles + +- **Commit** only when the user asks; **push** only when the user asks, and only branches. +- Never delete or force-push branches without an explicit request. +- Git runs over **SSH** with the user's existing key — never install or configure API + tokens (`gh auth login`, PATs); real enforcement is server-side branch protection on `test`. +- Commits: imperative subject ≤ 72 chars; agent-generated commits carry + `Co-Authored-By: `; never commit gitignored artifacts. +- Versions are marked with git tags — tags are the single source of truth. + +## Available scripts + +All commands run via `task` from the `scripts/` directory of this skill: + +```bash +# Compute the next branch name + detected base (no changes made) — show this to the user +task next -- --task-key="IIAA-123" --topic="api-cleanup" --repo-dir="/path/to/repo" + +# After user confirmation: fetch, then create the branch from origin/test (or origin/main) +task create -- --task-key="IIAA-123" --topic="api-cleanup" --repo-dir="/path/to/repo" + +# Build TypeScript to dist/ +task build +``` + +TypeScript CLI actions (run via `cli` module → `src/`): +- **`next`** (`src/actions/next/action.ts`) — resolves version, sequence, and base branch; outputs the branch name and its parts as YAML +- **`create`** (`src/actions/create/action.ts`) — same resolution, then `git fetch --prune` + `git checkout -b `; errors out if neither `origin/test` nor `origin/main` exists and no `--base` was given; never pushes + +## Workflow + +1. Confirm the target repo has at least one semver tag (`git tag --list`). If not, stop and ask which version to tag — do not invent one. +2. Run `task next` — it prints the computed branch name and the detected base branch. +3. **Show the branch name to the user and wait for confirmation.** +4. Run `task create` only after the user confirms. If it reports no `test`/`main` on the remote, ask the user and re-run with `--base`. +5. Pushing and the PR into `test` are the user's decision — hand over the compare URL, never merge. + +## Gotchas + +- Tags are compared with `--sort=-v:refname`; a malformed tag (e.g. `release-1`) is ignored, not an error. +- SEQ scans both local and `origin/` branches — `create` fetches first, but if using `next` alone on a stale repo, run `git fetch --prune` or the sequence can collide with an unseen remote branch. +- `task-key` must match `PROJECT-ID` shape (`^[A-Z][A-Z0-9]*-[A-Za-z0-9]+$`); `topic` must be kebab-case. +- `create` falls back to branching from the local ref when offline (fetch failure is non-fatal). diff --git a/scripts/.gitignore b/scripts/.gitignore new file mode 100644 index 0000000..5b98130 --- /dev/null +++ b/scripts/.gitignore @@ -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 diff --git a/scripts/.scripts/base/Taskfile.yml b/scripts/.scripts/base/Taskfile.yml new file mode 100644 index 0000000..a8d5c3f --- /dev/null +++ b/scripts/.scripts/base/Taskfile.yml @@ -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 diff --git a/scripts/.scripts/base/api/environment-show.sh b/scripts/.scripts/base/api/environment-show.sh new file mode 100644 index 0000000..036112d --- /dev/null +++ b/scripts/.scripts/base/api/environment-show.sh @@ -0,0 +1,9 @@ +#!/bin/bash +# ------------------------------------------------------------------------------------- +# +# +# +# ------------------------------------------------------------------------------------- +. ./.scripts/base/lib/--index.sh + +_base_environment_show "$@" diff --git a/scripts/.scripts/base/lib/--env-vars-reader.sh b/scripts/.scripts/base/lib/--env-vars-reader.sh new file mode 100644 index 0000000..ec67c93 --- /dev/null +++ b/scripts/.scripts/base/lib/--env-vars-reader.sh @@ -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[@]}" diff --git a/scripts/.scripts/base/lib/--env-vars-validator.sh b/scripts/.scripts/base/lib/--env-vars-validator.sh new file mode 100644 index 0000000..d55556d --- /dev/null +++ b/scripts/.scripts/base/lib/--env-vars-validator.sh @@ -0,0 +1,10 @@ +#!/bin/bash +# ------------------------------------------------------------------------------------- +# +# +# +# ------------------------------------------------------------------------------------- + +. ./.scripts/base/lib/--index-api.sh + +# No required environment variables for skill-manager base diff --git a/scripts/.scripts/base/lib/--index-api.sh b/scripts/.scripts/base/lib/--index-api.sh new file mode 100644 index 0000000..b6e223f --- /dev/null +++ b/scripts/.scripts/base/lib/--index-api.sh @@ -0,0 +1,12 @@ +#!/bin/bash +# ------------------------------------------------------------------------------------- +# +# +# +# ------------------------------------------------------------------------------------- + +. ./.scripts/base/lib/-ensure-environment-variable.sh + +. ./.scripts/base/lib/-environment-show.sh + +. ./.scripts/base/lib/-mask.sh diff --git a/scripts/.scripts/base/lib/--index.sh b/scripts/.scripts/base/lib/--index.sh new file mode 100644 index 0000000..45fdc99 --- /dev/null +++ b/scripts/.scripts/base/lib/--index.sh @@ -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 +# ------------------------------------------------------------------------------------- diff --git a/scripts/.scripts/base/lib/-ensure-environment-variable.sh b/scripts/.scripts/base/lib/-ensure-environment-variable.sh new file mode 100644 index 0000000..a745748 --- /dev/null +++ b/scripts/.scripts/base/lib/-ensure-environment-variable.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 +} diff --git a/scripts/.scripts/base/lib/-environment-show.sh b/scripts/.scripts/base/lib/-environment-show.sh new file mode 100644 index 0000000..aba067c --- /dev/null +++ b/scripts/.scripts/base/lib/-environment-show.sh @@ -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}" + +} diff --git a/scripts/.scripts/base/lib/-mask.sh b/scripts/.scripts/base/lib/-mask.sh new file mode 100644 index 0000000..11c110d --- /dev/null +++ b/scripts/.scripts/base/lib/-mask.sh @@ -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}" +} diff --git a/scripts/.scripts/cli/Taskfile.yml b/scripts/.scripts/cli/Taskfile.yml new file mode 100644 index 0000000..23cdc53 --- /dev/null +++ b/scripts/.scripts/cli/Taskfile.yml @@ -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 diff --git a/scripts/.scripts/cli/api/build.sh b/scripts/.scripts/cli/api/build.sh new file mode 100755 index 0000000..b897a67 --- /dev/null +++ b/scripts/.scripts/cli/api/build.sh @@ -0,0 +1,11 @@ +#!/bin/bash +# ------------------------------------------------------------------------------------- +# +# +# +# ------------------------------------------------------------------------------------- +set -e + +. ./.scripts/cli/lib/--index.sh + +_cli__build "$@" diff --git a/scripts/.scripts/cli/api/create.sh b/scripts/.scripts/cli/api/create.sh new file mode 100755 index 0000000..1ff1447 --- /dev/null +++ b/scripts/.scripts/cli/api/create.sh @@ -0,0 +1,11 @@ +#!/bin/bash +# ------------------------------------------------------------------------------------- +# +# +# +# ------------------------------------------------------------------------------------- +set -e + +. ./.scripts/cli/lib/--index.sh + +_cli__create "$@" diff --git a/scripts/.scripts/cli/api/next.sh b/scripts/.scripts/cli/api/next.sh new file mode 100755 index 0000000..24f7d5b --- /dev/null +++ b/scripts/.scripts/cli/api/next.sh @@ -0,0 +1,11 @@ +#!/bin/bash +# ------------------------------------------------------------------------------------- +# +# +# +# ------------------------------------------------------------------------------------- +set -e + +. ./.scripts/cli/lib/--index.sh + +_cli__next "$@" diff --git a/scripts/.scripts/cli/lib/--env-vars-reader.sh b/scripts/.scripts/cli/lib/--env-vars-reader.sh new file mode 100644 index 0000000..47e7e02 --- /dev/null +++ b/scripts/.scripts/cli/lib/--env-vars-reader.sh @@ -0,0 +1,15 @@ +#!/bin/bash +# ------------------------------------------------------------------------------------- +# +# +# +# ------------------------------------------------------------------------------------- + +ALL_ARGS=("$@") +while [[ "$#" -gt 0 ]]; do + case $1 in + *) ;; + esac + shift +done +set -- "${ALL_ARGS[@]}" diff --git a/scripts/.scripts/cli/lib/--env-vars-validator.sh b/scripts/.scripts/cli/lib/--env-vars-validator.sh new file mode 100644 index 0000000..0c28e45 --- /dev/null +++ b/scripts/.scripts/cli/lib/--env-vars-validator.sh @@ -0,0 +1,10 @@ +#!/bin/bash +# ------------------------------------------------------------------------------------- +# +# +# +# ------------------------------------------------------------------------------------- + +. ./.scripts/cli/lib/--index-api.sh + +# No required environment variables for cli module diff --git a/scripts/.scripts/cli/lib/--index-api.sh b/scripts/.scripts/cli/lib/--index-api.sh new file mode 100644 index 0000000..226f0f4 --- /dev/null +++ b/scripts/.scripts/cli/lib/--index-api.sh @@ -0,0 +1,12 @@ +#!/bin/bash +# ------------------------------------------------------------------------------------- +# +# +# +# ------------------------------------------------------------------------------------- + +. ./.scripts/cli/lib/-build.sh + +. ./.scripts/cli/lib/-next.sh + +. ./.scripts/cli/lib/-create.sh diff --git a/scripts/.scripts/cli/lib/--index.sh b/scripts/.scripts/cli/lib/--index.sh new file mode 100644 index 0000000..31f2275 --- /dev/null +++ b/scripts/.scripts/cli/lib/--index.sh @@ -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 +# ------------------------------------------------------------------------------------- diff --git a/scripts/.scripts/cli/lib/-build.sh b/scripts/.scripts/cli/lib/-build.sh new file mode 100644 index 0000000..00f9a4b --- /dev/null +++ b/scripts/.scripts/cli/lib/-build.sh @@ -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 +} diff --git a/scripts/.scripts/cli/lib/-create.sh b/scripts/.scripts/cli/lib/-create.sh new file mode 100644 index 0000000..598c570 --- /dev/null +++ b/scripts/.scripts/cli/lib/-create.sh @@ -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 "$@" +} diff --git a/scripts/.scripts/cli/lib/-next.sh b/scripts/.scripts/cli/lib/-next.sh new file mode 100644 index 0000000..2e61925 --- /dev/null +++ b/scripts/.scripts/cli/lib/-next.sh @@ -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 "$@" +} diff --git a/scripts/.scripts/loggers/lib/--env-vars-reader.sh b/scripts/.scripts/loggers/lib/--env-vars-reader.sh new file mode 100644 index 0000000..aec03dd --- /dev/null +++ b/scripts/.scripts/loggers/lib/--env-vars-reader.sh @@ -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}" diff --git a/scripts/.scripts/loggers/lib/--import.sh b/scripts/.scripts/loggers/lib/--import.sh new file mode 100644 index 0000000..d1e5edc --- /dev/null +++ b/scripts/.scripts/loggers/lib/--import.sh @@ -0,0 +1,9 @@ +#!/bin/bash +# ------------------------------------------------------------------------------------- +# +# TOBE ADDED - Licence & Copyright Disclaimer +# +# ------------------------------------------------------------------------------------- + +. ./.scripts/loggers/lib/--env-vars-reader.sh +. ./.scripts/loggers/lib/--index.sh diff --git a/scripts/.scripts/loggers/lib/--index-api.sh b/scripts/.scripts/loggers/lib/--index-api.sh new file mode 100644 index 0000000..66118fe --- /dev/null +++ b/scripts/.scripts/loggers/lib/--index-api.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 diff --git a/scripts/.scripts/loggers/lib/--index.sh b/scripts/.scripts/loggers/lib/--index.sh new file mode 100644 index 0000000..8952d78 --- /dev/null +++ b/scripts/.scripts/loggers/lib/--index.sh @@ -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 +# ------------------------------------------------------------------------------------- diff --git a/scripts/.scripts/loggers/lib/-debug.sh b/scripts/.scripts/loggers/lib/-debug.sh new file mode 100644 index 0000000..a4022c1 --- /dev/null +++ b/scripts/.scripts/loggers/lib/-debug.sh @@ -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 +} diff --git a/scripts/.scripts/loggers/lib/-empty-line.sh b/scripts/.scripts/loggers/lib/-empty-line.sh new file mode 100644 index 0000000..e44b21f --- /dev/null +++ b/scripts/.scripts/loggers/lib/-empty-line.sh @@ -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 +} diff --git a/scripts/.scripts/loggers/lib/-enable-trailing-new-line.sh b/scripts/.scripts/loggers/lib/-enable-trailing-new-line.sh new file mode 100644 index 0000000..5a281ac --- /dev/null +++ b/scripts/.scripts/loggers/lib/-enable-trailing-new-line.sh @@ -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 +} diff --git a/scripts/.scripts/loggers/lib/-error.sh b/scripts/.scripts/loggers/lib/-error.sh new file mode 100644 index 0000000..6fc040e --- /dev/null +++ b/scripts/.scripts/loggers/lib/-error.sh @@ -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 + +} diff --git a/scripts/.scripts/loggers/lib/-info.sh b/scripts/.scripts/loggers/lib/-info.sh new file mode 100644 index 0000000..f5b13e5 --- /dev/null +++ b/scripts/.scripts/loggers/lib/-info.sh @@ -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 +} diff --git a/scripts/.scripts/loggers/lib/-printf--debug.sh b/scripts/.scripts/loggers/lib/-printf--debug.sh new file mode 100644 index 0000000..1a44e33 --- /dev/null +++ b/scripts/.scripts/loggers/lib/-printf--debug.sh @@ -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 +} diff --git a/scripts/.scripts/loggers/lib/-printf--info.sh b/scripts/.scripts/loggers/lib/-printf--info.sh new file mode 100644 index 0000000..24bcd5b --- /dev/null +++ b/scripts/.scripts/loggers/lib/-printf--info.sh @@ -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 +} diff --git a/scripts/.scripts/loggers/lib/-printf--trace.sh b/scripts/.scripts/loggers/lib/-printf--trace.sh new file mode 100644 index 0000000..cdbe66b --- /dev/null +++ b/scripts/.scripts/loggers/lib/-printf--trace.sh @@ -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 +} diff --git a/scripts/.scripts/loggers/lib/-printf--warn.sh b/scripts/.scripts/loggers/lib/-printf--warn.sh new file mode 100644 index 0000000..a19138f --- /dev/null +++ b/scripts/.scripts/loggers/lib/-printf--warn.sh @@ -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 +} diff --git a/scripts/.scripts/loggers/lib/-trace.sh b/scripts/.scripts/loggers/lib/-trace.sh new file mode 100644 index 0000000..7b5916c --- /dev/null +++ b/scripts/.scripts/loggers/lib/-trace.sh @@ -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 + +} diff --git a/scripts/.scripts/loggers/lib/-waiting-dot.sh b/scripts/.scripts/loggers/lib/-waiting-dot.sh new file mode 100644 index 0000000..8f9787d --- /dev/null +++ b/scripts/.scripts/loggers/lib/-waiting-dot.sh @@ -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="" +} diff --git a/scripts/.scripts/loggers/lib/-warn.sh b/scripts/.scripts/loggers/lib/-warn.sh new file mode 100644 index 0000000..95bd71b --- /dev/null +++ b/scripts/.scripts/loggers/lib/-warn.sh @@ -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 +} diff --git a/scripts/Taskfile.yml b/scripts/Taskfile.yml new file mode 100644 index 0000000..1648d22 --- /dev/null +++ b/scripts/Taskfile.yml @@ -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 diff --git a/scripts/package.json b/scripts/package.json new file mode 100644 index 0000000..1dab38d --- /dev/null +++ b/scripts/package.json @@ -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" + } +} diff --git a/scripts/pnpm-lock.yaml b/scripts/pnpm-lock.yaml new file mode 100644 index 0000000..5079820 --- /dev/null +++ b/scripts/pnpm-lock.yaml @@ -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: {} diff --git a/scripts/src/actions/create/action.ts b/scripts/src/actions/create/action.ts new file mode 100644 index 0000000..2f79755 --- /dev/null +++ b/scripts/src/actions/create/action.ts @@ -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): Record { + 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", + }; +} diff --git a/scripts/src/actions/next/action.ts b/scripts/src/actions/next/action.ts new file mode 100644 index 0000000..78b113f --- /dev/null +++ b/scripts/src/actions/next/action.ts @@ -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 +): { branch?: string; error?: string; parts?: Record } { + 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): Record { + 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 } : {}, + }; +} diff --git a/scripts/src/cli/commands.ts b/scripts/src/cli/commands.ts new file mode 100644 index 0000000..c69caf1 --- /dev/null +++ b/scripts/src/cli/commands.ts @@ -0,0 +1,126 @@ +#!/usr/bin/env node +/** + * branch-tools CLI dispatcher + * + * Usage: + * branch-tools --action [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) => Record; +}; + +const ACTIONS: Record Promise> = { + next: () => import("../actions/next/action.js"), + create: () => import("../actions/create/action.js"), +}; + +// --------------------------------------------------------------------------- +// CLI +// --------------------------------------------------------------------------- + +async function main(): Promise { + 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 [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 Task identifier, e.g. IIAA-123 (required) + --topic Kebab-case focus topic, e.g. api-cleanup (required) + --base Base branch for create (default: origin/test, then origin/main) + --repo-dir 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(); diff --git a/scripts/tsconfig.json b/scripts/tsconfig.json new file mode 100644 index 0000000..7bad8a4 --- /dev/null +++ b/scripts/tsconfig.json @@ -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"] +}