mema-kit 1.1.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.js +33 -4
- package/package.json +2 -1
- package/scripts/bash/check-prerequisites.sh +144 -0
- package/scripts/bash/commit-changes.sh +147 -0
- package/scripts/bash/common.sh +122 -0
- package/scripts/bash/create-feature-branch.sh +160 -0
- package/scripts/bash/create-pr.sh +184 -0
- package/skills/mema.implement/SKILL.md +22 -3
- package/skills/mema.specify/SKILL.md +14 -0
package/bin/cli.js
CHANGED
|
@@ -10,6 +10,8 @@ const VERSION_COMMENT = `<!-- mema-kit v${VERSION} -->`;
|
|
|
10
10
|
const packageRoot = path.resolve(__dirname, '..');
|
|
11
11
|
const skillsSource = path.join(packageRoot, 'skills');
|
|
12
12
|
const targetDir = path.join(process.cwd(), '.claude', 'skills');
|
|
13
|
+
const scriptsSource = path.join(packageRoot, 'scripts', 'bash');
|
|
14
|
+
const scriptsTarget = path.join(process.cwd(), 'scripts', 'bash');
|
|
13
15
|
|
|
14
16
|
// Parse arguments
|
|
15
17
|
const args = process.argv.slice(2);
|
|
@@ -42,10 +44,13 @@ function runInstall() {
|
|
|
42
44
|
// Create target directory
|
|
43
45
|
fs.mkdirSync(targetDir, { recursive: true });
|
|
44
46
|
|
|
45
|
-
// Copy all skills
|
|
47
|
+
// Copy all skills and bash automation scripts
|
|
46
48
|
copySkills();
|
|
49
|
+
copyScripts();
|
|
47
50
|
|
|
48
|
-
console.log('✓ mema-kit
|
|
51
|
+
console.log('✓ mema-kit installed:');
|
|
52
|
+
console.log(' .claude/skills/ — Claude Code skills');
|
|
53
|
+
console.log(' scripts/bash/ — git automation scripts');
|
|
49
54
|
console.log('');
|
|
50
55
|
console.log('Next step: Open your project in Claude Code and run /mema.onboard');
|
|
51
56
|
console.log('');
|
|
@@ -75,10 +80,13 @@ function runUpdate() {
|
|
|
75
80
|
}
|
|
76
81
|
}
|
|
77
82
|
|
|
78
|
-
// Copy (overwrite) all skills
|
|
83
|
+
// Copy (overwrite) all skills and bash automation scripts
|
|
79
84
|
copySkills();
|
|
85
|
+
copyScripts();
|
|
80
86
|
|
|
81
|
-
console.log(`✓ mema-kit
|
|
87
|
+
console.log(`✓ mema-kit updated to v${VERSION}:`);
|
|
88
|
+
console.log(' .claude/skills/ — skills updated');
|
|
89
|
+
console.log(' scripts/bash/ — git automation scripts updated');
|
|
82
90
|
console.log('');
|
|
83
91
|
console.log(' .mema/ and CLAUDE.md were not modified.');
|
|
84
92
|
console.log('');
|
|
@@ -107,6 +115,27 @@ function copySkills() {
|
|
|
107
115
|
}
|
|
108
116
|
}
|
|
109
117
|
|
|
118
|
+
function copyScripts() {
|
|
119
|
+
// Create the target scripts/bash/ directory if it doesn't exist
|
|
120
|
+
fs.mkdirSync(scriptsTarget, { recursive: true });
|
|
121
|
+
|
|
122
|
+
const entries = fs.readdirSync(scriptsSource, { withFileTypes: true });
|
|
123
|
+
|
|
124
|
+
for (const entry of entries) {
|
|
125
|
+
// Only copy .sh files — do NOT inject version comments into bash scripts
|
|
126
|
+
if (!entry.isFile() || !entry.name.endsWith('.sh')) continue;
|
|
127
|
+
|
|
128
|
+
const sourcePath = path.join(scriptsSource, entry.name);
|
|
129
|
+
const targetPath = path.join(scriptsTarget, entry.name);
|
|
130
|
+
|
|
131
|
+
fs.writeFileSync(targetPath, fs.readFileSync(sourcePath));
|
|
132
|
+
|
|
133
|
+
// Preserve execute permissions — Node's writeFileSync does not copy the source
|
|
134
|
+
// file's mode, so scripts would be non-executable without this step
|
|
135
|
+
fs.chmodSync(targetPath, 0o755);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
110
139
|
function addVersionComment(content) {
|
|
111
140
|
// Remove any existing version comment
|
|
112
141
|
const cleaned = content.replace(/<!-- mema-kit v[\d.]+ -->\n?/, '');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mema-kit",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "AI-assisted development lifecycle kit",
|
|
5
5
|
"bin": {
|
|
6
6
|
"mema-kit": "bin/cli.js"
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"docs/",
|
|
10
10
|
"bin/",
|
|
11
11
|
"skills/",
|
|
12
|
+
"scripts/",
|
|
12
13
|
"templates/"
|
|
13
14
|
],
|
|
14
15
|
"keywords": [
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# check-prerequisites.sh — Validates environment prerequisites for mema-kit git automation.
|
|
3
|
+
#
|
|
4
|
+
# Called by git automation scripts before running to ensure git, gh, and repo
|
|
5
|
+
# state are suitable. Also useful as a standalone diagnostic tool.
|
|
6
|
+
#
|
|
7
|
+
# USAGE:
|
|
8
|
+
# check-prerequisites.sh [--need-gh] [--json]
|
|
9
|
+
#
|
|
10
|
+
# OPTIONS:
|
|
11
|
+
# --need-gh Also check that gh CLI is installed and authenticated
|
|
12
|
+
# --json Emit results as JSON; messages go to stdout
|
|
13
|
+
#
|
|
14
|
+
# MODES:
|
|
15
|
+
# Flag mode (--need-gh present): runs checks silently; exits 1 on first failure
|
|
16
|
+
# Summary mode (no --need-gh): prints pass/fail for all four checks; always exits 0
|
|
17
|
+
#
|
|
18
|
+
# OUTPUT — text mode, flag:
|
|
19
|
+
# [silent on success]
|
|
20
|
+
# [missing] Required tool not found: git
|
|
21
|
+
# [error] Not inside a git repository — run from your project root
|
|
22
|
+
# [missing] Required tool not found: gh
|
|
23
|
+
# [error] gh CLI is not authenticated — run: gh auth login
|
|
24
|
+
#
|
|
25
|
+
# OUTPUT — text mode, summary:
|
|
26
|
+
# [ok] git is available
|
|
27
|
+
# [ok] inside a git repository
|
|
28
|
+
# [ok] gh CLI is available
|
|
29
|
+
# [ok] gh CLI is authenticated
|
|
30
|
+
#
|
|
31
|
+
# EXIT CODES:
|
|
32
|
+
# 0 — all checks passed (flag mode), or summary printed (summary mode)
|
|
33
|
+
# 1 — a required check failed (flag mode only)
|
|
34
|
+
|
|
35
|
+
# ---------------------------------------------------------------------------
|
|
36
|
+
# Self-locate and source shared library.
|
|
37
|
+
# ${BASH_SOURCE[0]} is more reliable than $0 when invoked via symlinks or
|
|
38
|
+
# called with an explicit path from a different working directory.
|
|
39
|
+
# ---------------------------------------------------------------------------
|
|
40
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
41
|
+
source "${SCRIPT_DIR}/common.sh"
|
|
42
|
+
# common.sh activates: set -euo pipefail, git_root, output_result,
|
|
43
|
+
# check_prereq, parse_common_args
|
|
44
|
+
|
|
45
|
+
# ---------------------------------------------------------------------------
|
|
46
|
+
# Pass 1: parse common args.
|
|
47
|
+
# Sets OUTPUT_FORMAT=json if --json is present so all output_result calls
|
|
48
|
+
# below automatically emit JSON.
|
|
49
|
+
# ---------------------------------------------------------------------------
|
|
50
|
+
parse_common_args "$@"
|
|
51
|
+
|
|
52
|
+
# ---------------------------------------------------------------------------
|
|
53
|
+
# Pass 2: parse script-specific args.
|
|
54
|
+
# Detect --need-gh to know whether to run gh checks and which mode to use.
|
|
55
|
+
# ---------------------------------------------------------------------------
|
|
56
|
+
NEED_GH=0
|
|
57
|
+
|
|
58
|
+
for arg in "$@"; do
|
|
59
|
+
case "$arg" in
|
|
60
|
+
--need-gh) NEED_GH=1 ;;
|
|
61
|
+
*) ;; # --json already handled; unknown flags silently ignored
|
|
62
|
+
esac
|
|
63
|
+
done
|
|
64
|
+
|
|
65
|
+
# ---------------------------------------------------------------------------
|
|
66
|
+
# Mode selection.
|
|
67
|
+
# Summary mode = NEED_GH is 0 (no operational flags; --json alone doesn't count)
|
|
68
|
+
# → print pass/fail for all four checks; always exit 0
|
|
69
|
+
# Flag mode = NEED_GH is 1
|
|
70
|
+
# → run only the requested checks; silent on success; exit 1 on first failure
|
|
71
|
+
# ---------------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
if [[ "$NEED_GH" -eq 0 ]]; then
|
|
74
|
+
|
|
75
|
+
# -------------------------------------------------------------------------
|
|
76
|
+
# SUMMARY MODE
|
|
77
|
+
# Run every check independently (no exit on failure) and print a status
|
|
78
|
+
# line for each. Useful for standalone debugging: bash check-prerequisites.sh
|
|
79
|
+
# -------------------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
# Check 1: git on PATH
|
|
82
|
+
if command -v git &>/dev/null; then
|
|
83
|
+
output_result "ok" "git is available"
|
|
84
|
+
else
|
|
85
|
+
output_result "missing" "git not found — install git to use mema-kit scripts" ', "check": "git"'
|
|
86
|
+
fi
|
|
87
|
+
|
|
88
|
+
# Check 2: inside a git repository
|
|
89
|
+
if git rev-parse --show-toplevel &>/dev/null 2>&1; then
|
|
90
|
+
output_result "ok" "inside a git repository"
|
|
91
|
+
else
|
|
92
|
+
output_result "error" "not inside a git repository — run from your project root" ', "check": "git-repo"'
|
|
93
|
+
fi
|
|
94
|
+
|
|
95
|
+
# Check 3: gh CLI on PATH
|
|
96
|
+
if command -v gh &>/dev/null; then
|
|
97
|
+
output_result "ok" "gh CLI is available"
|
|
98
|
+
else
|
|
99
|
+
output_result "missing" "gh CLI not found — install from https://cli.github.com" ', "check": "gh"'
|
|
100
|
+
fi
|
|
101
|
+
|
|
102
|
+
# Check 4: gh authenticated
|
|
103
|
+
# Guard: only attempt auth check if gh is present — avoids "command not found"
|
|
104
|
+
# noise when gh is not installed.
|
|
105
|
+
if ! command -v gh &>/dev/null; then
|
|
106
|
+
output_result "skipped" "gh CLI not installed — skipping auth check" ', "check": "gh-auth"'
|
|
107
|
+
elif gh auth status &>/dev/null 2>&1; then
|
|
108
|
+
output_result "ok" "gh CLI is authenticated"
|
|
109
|
+
else
|
|
110
|
+
output_result "error" "gh CLI is not authenticated — run: gh auth login" ', "check": "gh-auth"'
|
|
111
|
+
fi
|
|
112
|
+
|
|
113
|
+
exit 0
|
|
114
|
+
|
|
115
|
+
fi
|
|
116
|
+
|
|
117
|
+
# ---------------------------------------------------------------------------
|
|
118
|
+
# FLAG MODE
|
|
119
|
+
# Run only the checks appropriate for the requested flags.
|
|
120
|
+
# Silent on success; output_result + exit 1 on first failure.
|
|
121
|
+
# Callers use: check-prerequisites.sh --need-gh || exit 1
|
|
122
|
+
# ---------------------------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
# Universal checks: git and repo state are always validated in flag mode.
|
|
125
|
+
check_prereq git || exit 1
|
|
126
|
+
|
|
127
|
+
if ! git rev-parse --show-toplevel &>/dev/null 2>&1; then
|
|
128
|
+
output_result "error" "Not inside a git repository — run from your project root" ', "check": "git-repo"'
|
|
129
|
+
exit 1
|
|
130
|
+
fi
|
|
131
|
+
|
|
132
|
+
# gh checks: only when --need-gh was passed (always true in flag mode currently,
|
|
133
|
+
# but kept explicit for future extensibility).
|
|
134
|
+
if [[ "$NEED_GH" -eq 1 ]]; then
|
|
135
|
+
check_prereq gh || exit 1
|
|
136
|
+
|
|
137
|
+
if ! gh auth status &>/dev/null 2>&1; then
|
|
138
|
+
output_result "error" "gh CLI is not authenticated — run: gh auth login" ', "check": "gh-auth"'
|
|
139
|
+
exit 1
|
|
140
|
+
fi
|
|
141
|
+
fi
|
|
142
|
+
|
|
143
|
+
# All checks passed — exit silently.
|
|
144
|
+
exit 0
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# commit-changes.sh — Validates a Conventional Commits message, shows the staged
|
|
3
|
+
# diff, runs git commit, and pushes to the current branch.
|
|
4
|
+
#
|
|
5
|
+
# Called by /mema.implement at the end of the per-feature cycle, after all tasks
|
|
6
|
+
# are complete and the user confirms their tests pass.
|
|
7
|
+
#
|
|
8
|
+
# USAGE:
|
|
9
|
+
# commit-changes.sh [--json] <commit-message>
|
|
10
|
+
#
|
|
11
|
+
# ARGUMENTS:
|
|
12
|
+
# commit-message — Quoted Conventional Commits message (Claude generates this).
|
|
13
|
+
# Format: type(scope): description
|
|
14
|
+
# Example: feat(cli): add --json flag to output
|
|
15
|
+
#
|
|
16
|
+
# OPTIONS:
|
|
17
|
+
# --json Emit result as JSON to stdout; all other messages go to stderr.
|
|
18
|
+
#
|
|
19
|
+
# OUTPUT — text mode:
|
|
20
|
+
# [ok] Committed: abc1234 — feat(cli): add --json flag to output
|
|
21
|
+
# [error] Commit message format invalid. Expected: type(scope): description
|
|
22
|
+
# [error] Nothing is staged. Stage your changes before committing.
|
|
23
|
+
#
|
|
24
|
+
# OUTPUT — json mode:
|
|
25
|
+
# {"status": "ok", "message": "...", "committed": true, "sha": "abc1234", "branch": "feat-003-name", "commit_message": "feat: initial implementation"}
|
|
26
|
+
# {"status": "error", "message": "..."}
|
|
27
|
+
#
|
|
28
|
+
# EXIT CODES:
|
|
29
|
+
# 0 — commit and push succeeded
|
|
30
|
+
# 1 — validation failed, nothing staged, or unexpected error
|
|
31
|
+
|
|
32
|
+
# ---------------------------------------------------------------------------
|
|
33
|
+
# Self-locate and source shared library.
|
|
34
|
+
# ${BASH_SOURCE[0]} is more reliable than $0 when invoked via symlinks or
|
|
35
|
+
# called with an explicit path from a different working directory.
|
|
36
|
+
# ---------------------------------------------------------------------------
|
|
37
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
38
|
+
source "${SCRIPT_DIR}/common.sh"
|
|
39
|
+
# common.sh activates: set -euo pipefail, git_root, output_result,
|
|
40
|
+
# check_prereq, parse_common_args
|
|
41
|
+
|
|
42
|
+
# ---------------------------------------------------------------------------
|
|
43
|
+
# Pass 1: Parse common args.
|
|
44
|
+
# parse_common_args uses a for-loop and does NOT consume "$@", so the full
|
|
45
|
+
# argument list is still available for our own parsing pass below.
|
|
46
|
+
# Sets OUTPUT_FORMAT=json if --json is present.
|
|
47
|
+
# ---------------------------------------------------------------------------
|
|
48
|
+
parse_common_args "$@"
|
|
49
|
+
|
|
50
|
+
# Verify git is available before doing anything else.
|
|
51
|
+
check_prereq git || exit 1
|
|
52
|
+
|
|
53
|
+
# ---------------------------------------------------------------------------
|
|
54
|
+
# Pass 2: Parse script-specific args.
|
|
55
|
+
# Extracts the commit message positional arg. --json is re-encountered here
|
|
56
|
+
# and skipped — it was already handled by parse_common_args above.
|
|
57
|
+
# ---------------------------------------------------------------------------
|
|
58
|
+
commit_message=""
|
|
59
|
+
|
|
60
|
+
while [[ $# -gt 0 ]]; do
|
|
61
|
+
case "$1" in
|
|
62
|
+
--json)
|
|
63
|
+
# Already handled by parse_common_args; skip.
|
|
64
|
+
shift
|
|
65
|
+
;;
|
|
66
|
+
-*)
|
|
67
|
+
# Unknown flag — silently ignore to stay forward-compatible.
|
|
68
|
+
shift
|
|
69
|
+
;;
|
|
70
|
+
*)
|
|
71
|
+
# Positional argument: the commit message (take the first one found).
|
|
72
|
+
if [[ -z "$commit_message" ]]; then
|
|
73
|
+
commit_message="$1"
|
|
74
|
+
fi
|
|
75
|
+
shift
|
|
76
|
+
;;
|
|
77
|
+
esac
|
|
78
|
+
done
|
|
79
|
+
|
|
80
|
+
# ---------------------------------------------------------------------------
|
|
81
|
+
# Input validation: require a non-empty commit message.
|
|
82
|
+
# ---------------------------------------------------------------------------
|
|
83
|
+
if [[ -z "$commit_message" ]]; then
|
|
84
|
+
echo "Error: missing required argument <commit-message>" >&2
|
|
85
|
+
echo "Usage: commit-changes.sh [--json] <commit-message>" >&2
|
|
86
|
+
echo "Example: commit-changes.sh \"feat(cli): add --json flag to output\"" >&2
|
|
87
|
+
exit 1
|
|
88
|
+
fi
|
|
89
|
+
|
|
90
|
+
# ---------------------------------------------------------------------------
|
|
91
|
+
# Conventional Commits format validation.
|
|
92
|
+
# Pattern: type(optional-scope): description (description 1–72 chars)
|
|
93
|
+
# This check runs before any git operation so that an invalid message
|
|
94
|
+
# is rejected without touching the repository.
|
|
95
|
+
#
|
|
96
|
+
# NOTE: The regex variable must NOT be quoted in the =~ test; quoting it
|
|
97
|
+
# would cause bash to treat it as a literal string rather than a pattern.
|
|
98
|
+
# ---------------------------------------------------------------------------
|
|
99
|
+
COMMIT_PATTERN='^(feat|fix|docs|style|refactor|test|chore|build|ci|perf|revert)(\(.+\))?: .{1,72}$'
|
|
100
|
+
if [[ ! "$commit_message" =~ $COMMIT_PATTERN ]]; then
|
|
101
|
+
output_result "error" \
|
|
102
|
+
"Commit message format invalid. Expected: type(scope): description — e.g. feat(cli): add --json flag"
|
|
103
|
+
exit 1
|
|
104
|
+
fi
|
|
105
|
+
|
|
106
|
+
# ---------------------------------------------------------------------------
|
|
107
|
+
# Staged diff check.
|
|
108
|
+
# git diff --staged --stat shows a human-readable summary of staged files.
|
|
109
|
+
# If the output is empty, nothing is staged and we should not create an empty
|
|
110
|
+
# commit. Diagnostic output always goes to stderr so it never taints JSON stdout.
|
|
111
|
+
# ---------------------------------------------------------------------------
|
|
112
|
+
staged="$(git diff --staged --stat)"
|
|
113
|
+
if [[ -z "$staged" ]]; then
|
|
114
|
+
output_result "error" \
|
|
115
|
+
"Nothing is staged. Use git add to stage your changes before committing."
|
|
116
|
+
exit 1
|
|
117
|
+
fi
|
|
118
|
+
|
|
119
|
+
echo "Staged changes:" >&2
|
|
120
|
+
echo "$staged" >&2
|
|
121
|
+
|
|
122
|
+
# ---------------------------------------------------------------------------
|
|
123
|
+
# Commit.
|
|
124
|
+
# set -euo pipefail (inherited from common.sh) ensures any failure here exits
|
|
125
|
+
# immediately with a non-zero code rather than silently continuing.
|
|
126
|
+
# ---------------------------------------------------------------------------
|
|
127
|
+
git commit -m "$commit_message"
|
|
128
|
+
|
|
129
|
+
# ---------------------------------------------------------------------------
|
|
130
|
+
# Push.
|
|
131
|
+
# --set-upstream is idempotent: sets the remote tracking branch on the first
|
|
132
|
+
# push, and is effectively a no-op on subsequent pushes if upstream is already
|
|
133
|
+
# set. This handles the case where create-feature-branch.sh created a local
|
|
134
|
+
# branch that has never yet been pushed to origin.
|
|
135
|
+
# ---------------------------------------------------------------------------
|
|
136
|
+
current_branch="$(git rev-parse --abbrev-ref HEAD)"
|
|
137
|
+
git push --set-upstream origin "$current_branch"
|
|
138
|
+
|
|
139
|
+
# ---------------------------------------------------------------------------
|
|
140
|
+
# Capture the short SHA immediately after commit so the output reflects the
|
|
141
|
+
# exact commit that was just created.
|
|
142
|
+
# ---------------------------------------------------------------------------
|
|
143
|
+
sha="$(git rev-parse --short HEAD)"
|
|
144
|
+
|
|
145
|
+
output_result "ok" \
|
|
146
|
+
"Committed: ${sha} — ${commit_message}" \
|
|
147
|
+
", \"committed\": true, \"sha\": \"${sha}\", \"branch\": \"${current_branch}\", \"commit_message\": \"${commit_message}\""
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# common.sh — Shared bash library for mema-kit git automation scripts.
|
|
3
|
+
#
|
|
4
|
+
# USAGE (from any git automation script in scripts/bash/):
|
|
5
|
+
# SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
6
|
+
# source "${SCRIPT_DIR}/common.sh"
|
|
7
|
+
#
|
|
8
|
+
# After sourcing, call parse_common_args "$@" to handle the --json flag,
|
|
9
|
+
# then use git_root, output_result, and check_prereq as needed.
|
|
10
|
+
|
|
11
|
+
# ---------------------------------------------------------------------------
|
|
12
|
+
# Double-source guard — safe to source multiple times.
|
|
13
|
+
#
|
|
14
|
+
# Uses ${COMMON_SH_LOADED:-} instead of $COMMON_SH_LOADED to avoid an
|
|
15
|
+
# "unbound variable" error from set -u on the first source, before COMMON_SH_LOADED
|
|
16
|
+
# has been defined.
|
|
17
|
+
# ---------------------------------------------------------------------------
|
|
18
|
+
[[ -n "${COMMON_SH_LOADED:-}" ]] && return 0
|
|
19
|
+
COMMON_SH_LOADED=1
|
|
20
|
+
|
|
21
|
+
# Strict mode: exit on any error (-e), treat unbound variables as errors (-u),
|
|
22
|
+
# and propagate pipe failures (-o pipefail). Scripts that source this file
|
|
23
|
+
# inherit these settings automatically.
|
|
24
|
+
set -euo pipefail
|
|
25
|
+
|
|
26
|
+
# ---------------------------------------------------------------------------
|
|
27
|
+
# git_root
|
|
28
|
+
#
|
|
29
|
+
# Prints the absolute path of the current git repository root to stdout.
|
|
30
|
+
# Exits non-zero with an error message to stderr if not inside a git repo.
|
|
31
|
+
#
|
|
32
|
+
# Example:
|
|
33
|
+
# root="$(git_root)" || exit 1
|
|
34
|
+
# cd "$root"
|
|
35
|
+
# ---------------------------------------------------------------------------
|
|
36
|
+
git_root() {
|
|
37
|
+
# 2>/dev/null suppresses git's own "not a git repository" error message
|
|
38
|
+
# so we can emit a consistent error instead.
|
|
39
|
+
git rev-parse --show-toplevel 2>/dev/null || {
|
|
40
|
+
echo "Error: not inside a git repository" >&2
|
|
41
|
+
return 1
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
# ---------------------------------------------------------------------------
|
|
46
|
+
# output_result <status> <message> [extra_json]
|
|
47
|
+
#
|
|
48
|
+
# Prints a result in JSON or plain-text format depending on OUTPUT_FORMAT.
|
|
49
|
+
#
|
|
50
|
+
# Arguments:
|
|
51
|
+
# status — short status string (e.g. "ok", "error", "missing")
|
|
52
|
+
# message — human-readable description
|
|
53
|
+
# extra_json — optional; pre-formatted JSON fragment starting with ", "
|
|
54
|
+
# e.g. ', "tool": "gh"' is appended inside the JSON object
|
|
55
|
+
#
|
|
56
|
+
# Output format is controlled by OUTPUT_FORMAT (default: text):
|
|
57
|
+
# text → [status] message
|
|
58
|
+
# json → {"status": "…", "message": "…"[, extra_json fields]}
|
|
59
|
+
#
|
|
60
|
+
# Set OUTPUT_FORMAT=json by calling parse_common_args "$@" at the top of
|
|
61
|
+
# each script, or by setting it directly before calling output_result.
|
|
62
|
+
#
|
|
63
|
+
# Examples:
|
|
64
|
+
# output_result "ok" "Branch created: 001-my-feature"
|
|
65
|
+
# output_result "missing" "Tool not found: gh" ', "tool": "gh"'
|
|
66
|
+
# ---------------------------------------------------------------------------
|
|
67
|
+
output_result() {
|
|
68
|
+
local status="$1"
|
|
69
|
+
local message="$2"
|
|
70
|
+
local extra="${3:-}" # optional extra JSON fragment, e.g. ', "branch": "001-foo"'
|
|
71
|
+
|
|
72
|
+
if [[ "${OUTPUT_FORMAT:-text}" == "json" ]]; then
|
|
73
|
+
echo "{\"status\": \"${status}\", \"message\": \"${message}\"${extra}}"
|
|
74
|
+
else
|
|
75
|
+
echo "[${status}] ${message}"
|
|
76
|
+
fi
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
# ---------------------------------------------------------------------------
|
|
80
|
+
# check_prereq <tool>
|
|
81
|
+
#
|
|
82
|
+
# Checks that <tool> is available on $PATH.
|
|
83
|
+
# Returns 0 if found; calls output_result and returns 1 if missing.
|
|
84
|
+
#
|
|
85
|
+
# Example:
|
|
86
|
+
# check_prereq git || exit 1
|
|
87
|
+
# check_prereq gh || exit 1 # non-fatal: caller may choose to degrade
|
|
88
|
+
# ---------------------------------------------------------------------------
|
|
89
|
+
check_prereq() {
|
|
90
|
+
local tool="$1"
|
|
91
|
+
|
|
92
|
+
if ! command -v "$tool" &>/dev/null; then
|
|
93
|
+
# ', "tool": "gh"' extends the JSON object so callers can read the tool name
|
|
94
|
+
output_result "missing" "Required tool not found: ${tool}" ", \"tool\": \"${tool}\""
|
|
95
|
+
return 1
|
|
96
|
+
fi
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
# ---------------------------------------------------------------------------
|
|
100
|
+
# parse_common_args "$@"
|
|
101
|
+
#
|
|
102
|
+
# Parses arguments shared across all git automation scripts.
|
|
103
|
+
# Currently handles:
|
|
104
|
+
# --json sets OUTPUT_FORMAT=json so output_result emits JSON
|
|
105
|
+
#
|
|
106
|
+
# NOTE: Call this at the top of each script, before using output_result,
|
|
107
|
+
# so that the output format is set correctly for all subsequent calls.
|
|
108
|
+
# Unknown flags are silently ignored — scripts can extend this with their
|
|
109
|
+
# own argument parsing after calling parse_common_args.
|
|
110
|
+
#
|
|
111
|
+
# Example:
|
|
112
|
+
# parse_common_args "$@"
|
|
113
|
+
# # OUTPUT_FORMAT is now set; proceed to parse script-specific args
|
|
114
|
+
# ---------------------------------------------------------------------------
|
|
115
|
+
parse_common_args() {
|
|
116
|
+
for arg in "$@"; do
|
|
117
|
+
case "$arg" in
|
|
118
|
+
--json) OUTPUT_FORMAT=json ;;
|
|
119
|
+
*) ;; # ignore unknown flags
|
|
120
|
+
esac
|
|
121
|
+
done
|
|
122
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# create-feature-branch.sh — Creates a new git feature branch for the mema-kit per-feature cycle.
|
|
3
|
+
#
|
|
4
|
+
# Called by /mema.specify at the start of a new feature to create a consistently
|
|
5
|
+
# named local branch. Designed to be idempotent and safe to re-run.
|
|
6
|
+
#
|
|
7
|
+
# USAGE:
|
|
8
|
+
# create-feature-branch.sh <NNN> <feature-name> [--short-name <name>] [--json]
|
|
9
|
+
#
|
|
10
|
+
# ARGUMENTS:
|
|
11
|
+
# NNN — Feature number (e.g. "002")
|
|
12
|
+
# feature-name — Kebab-case feature name (e.g. "create-feature-branch")
|
|
13
|
+
#
|
|
14
|
+
# OPTIONS:
|
|
15
|
+
# --short-name <name> Override the branch name suffix (default: feature-name arg)
|
|
16
|
+
# --json Emit result as JSON to stdout; all other messages go to stderr
|
|
17
|
+
#
|
|
18
|
+
# OUTPUT — text mode:
|
|
19
|
+
# [created] Branch created: feat-002-create-feature-branch
|
|
20
|
+
# [exists] Branch already exists: feat-002-create-feature-branch
|
|
21
|
+
# [error] Working directory is dirty — commit or stash changes before creating a branch
|
|
22
|
+
#
|
|
23
|
+
# OUTPUT — json mode:
|
|
24
|
+
# {"status": "created", "message": "...", "branch": "feat-002-name", "base": "main"}
|
|
25
|
+
# {"status": "exists", "message": "...", "branch": "feat-002-name", "base": "main"}
|
|
26
|
+
# {"status": "error", "message": "..."}
|
|
27
|
+
#
|
|
28
|
+
# EXIT CODES:
|
|
29
|
+
# 0 — branch created or already existed (idempotent success)
|
|
30
|
+
# 1 — dirty working directory, missing args, missing git, or unexpected error
|
|
31
|
+
|
|
32
|
+
# ---------------------------------------------------------------------------
|
|
33
|
+
# Self-locate and source shared library.
|
|
34
|
+
# ${BASH_SOURCE[0]} is more reliable than $0 when invoked via symlinks or
|
|
35
|
+
# called with an explicit path from a different working directory.
|
|
36
|
+
# ---------------------------------------------------------------------------
|
|
37
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
38
|
+
source "${SCRIPT_DIR}/common.sh"
|
|
39
|
+
# common.sh activates: set -euo pipefail, git_root, output_result,
|
|
40
|
+
# check_prereq, parse_common_args
|
|
41
|
+
|
|
42
|
+
# ---------------------------------------------------------------------------
|
|
43
|
+
# Pass 1: Parse common args.
|
|
44
|
+
# parse_common_args uses a for-loop and does NOT consume "$@", so the full
|
|
45
|
+
# argument list is still available for our own parsing pass below.
|
|
46
|
+
# Sets OUTPUT_FORMAT=json if --json is present.
|
|
47
|
+
# ---------------------------------------------------------------------------
|
|
48
|
+
parse_common_args "$@"
|
|
49
|
+
|
|
50
|
+
# Verify git is available before doing anything else.
|
|
51
|
+
check_prereq git || exit 1
|
|
52
|
+
|
|
53
|
+
# ---------------------------------------------------------------------------
|
|
54
|
+
# Pass 2: Parse script-specific args.
|
|
55
|
+
# Extracts positional args and --short-name. --json is re-encountered here
|
|
56
|
+
# and skipped — it was already handled by parse_common_args above.
|
|
57
|
+
# ---------------------------------------------------------------------------
|
|
58
|
+
feature_num=""
|
|
59
|
+
feature_name=""
|
|
60
|
+
short_name_override=""
|
|
61
|
+
|
|
62
|
+
while [[ $# -gt 0 ]]; do
|
|
63
|
+
case "$1" in
|
|
64
|
+
--json)
|
|
65
|
+
# Already handled by parse_common_args; skip.
|
|
66
|
+
shift
|
|
67
|
+
;;
|
|
68
|
+
--short-name)
|
|
69
|
+
# Guard against --short-name with no following value.
|
|
70
|
+
if [[ $# -lt 2 ]]; then
|
|
71
|
+
echo "Error: --short-name requires a value" >&2
|
|
72
|
+
exit 1
|
|
73
|
+
fi
|
|
74
|
+
short_name_override="$2"
|
|
75
|
+
shift 2
|
|
76
|
+
;;
|
|
77
|
+
-*)
|
|
78
|
+
# Unknown flag — silently ignore to stay forward-compatible.
|
|
79
|
+
shift
|
|
80
|
+
;;
|
|
81
|
+
*)
|
|
82
|
+
# Positional argument: first is NNN, second is feature-name.
|
|
83
|
+
if [[ -z "$feature_num" ]]; then
|
|
84
|
+
feature_num="$1"
|
|
85
|
+
elif [[ -z "$feature_name" ]]; then
|
|
86
|
+
feature_name="$1"
|
|
87
|
+
fi
|
|
88
|
+
shift
|
|
89
|
+
;;
|
|
90
|
+
esac
|
|
91
|
+
done
|
|
92
|
+
|
|
93
|
+
# ---------------------------------------------------------------------------
|
|
94
|
+
# Input validation.
|
|
95
|
+
# Both a feature number and a branch name suffix are required. The suffix
|
|
96
|
+
# comes from either <feature-name> or --short-name.
|
|
97
|
+
# ---------------------------------------------------------------------------
|
|
98
|
+
if [[ -z "$feature_num" ]]; then
|
|
99
|
+
echo "Error: missing required argument <NNN> (feature number, e.g. 002)" >&2
|
|
100
|
+
echo "Usage: create-feature-branch.sh <NNN> <feature-name> [--short-name <name>] [--json]" >&2
|
|
101
|
+
exit 1
|
|
102
|
+
fi
|
|
103
|
+
|
|
104
|
+
if [[ -z "$feature_name" && -z "$short_name_override" ]]; then
|
|
105
|
+
echo "Error: missing required argument <feature-name> (or use --short-name <name>)" >&2
|
|
106
|
+
echo "Usage: create-feature-branch.sh <NNN> <feature-name> [--short-name <name>] [--json]" >&2
|
|
107
|
+
exit 1
|
|
108
|
+
fi
|
|
109
|
+
|
|
110
|
+
# ---------------------------------------------------------------------------
|
|
111
|
+
# Capture base branch and construct target branch name.
|
|
112
|
+
# base_branch is captured BEFORE any git operations so the output correctly
|
|
113
|
+
# records the branch we diverged from, even after we switch branches.
|
|
114
|
+
# ---------------------------------------------------------------------------
|
|
115
|
+
base_branch="$(git rev-parse --abbrev-ref HEAD)"
|
|
116
|
+
|
|
117
|
+
# Use --short-name override if provided; fall back to the feature-name positional arg.
|
|
118
|
+
branch_name="feat-${feature_num}-${short_name_override:-${feature_name}}"
|
|
119
|
+
|
|
120
|
+
# ---------------------------------------------------------------------------
|
|
121
|
+
# Dirty-dir guard.
|
|
122
|
+
# git status --porcelain is intentionally conservative: it reports untracked
|
|
123
|
+
# files (??), staged changes, and unstaged changes. Any output means the
|
|
124
|
+
# working directory is not clean enough for a safe branch creation.
|
|
125
|
+
# The dirty file list always goes to stderr so it never taints JSON stdout.
|
|
126
|
+
# ---------------------------------------------------------------------------
|
|
127
|
+
dirty="$(git status --porcelain)"
|
|
128
|
+
if [[ -n "$dirty" ]]; then
|
|
129
|
+
echo "Dirty files:" >&2
|
|
130
|
+
echo "$dirty" >&2
|
|
131
|
+
output_result "error" "Working directory is dirty — commit or stash changes before creating a branch"
|
|
132
|
+
exit 1
|
|
133
|
+
fi
|
|
134
|
+
|
|
135
|
+
# ---------------------------------------------------------------------------
|
|
136
|
+
# Idempotency check.
|
|
137
|
+
# git branch --list "name" emits the branch name (with leading whitespace)
|
|
138
|
+
# if it exists locally, or empty output if it does not. grep -q . tests for
|
|
139
|
+
# any non-empty output (the dot matches any character, so any output passes).
|
|
140
|
+
# If the branch exists: checkout (no-op if already on it; git returns 0) and exit 0.
|
|
141
|
+
# ---------------------------------------------------------------------------
|
|
142
|
+
if git branch --list "${branch_name}" | grep -q .; then
|
|
143
|
+
git checkout "${branch_name}"
|
|
144
|
+
output_result "exists" \
|
|
145
|
+
"Branch already exists: ${branch_name}" \
|
|
146
|
+
", \"branch\": \"${branch_name}\", \"base\": \"${base_branch}\""
|
|
147
|
+
exit 0
|
|
148
|
+
fi
|
|
149
|
+
|
|
150
|
+
# ---------------------------------------------------------------------------
|
|
151
|
+
# Branch creation.
|
|
152
|
+
# git checkout -b creates the branch and switches to it in one atomic step.
|
|
153
|
+
# set -euo pipefail (inherited from common.sh) ensures any failure exits
|
|
154
|
+
# immediately with a non-zero code rather than silently continuing.
|
|
155
|
+
# ---------------------------------------------------------------------------
|
|
156
|
+
git checkout -b "${branch_name}"
|
|
157
|
+
|
|
158
|
+
output_result "created" \
|
|
159
|
+
"Branch created: ${branch_name}" \
|
|
160
|
+
", \"branch\": \"${branch_name}\", \"base\": \"${base_branch}\""
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# create-pr.sh — Creates a draft GitHub pull request for the current feature branch.
|
|
3
|
+
#
|
|
4
|
+
# Called by /mema.implement after all tasks are complete and the branch has been
|
|
5
|
+
# committed and pushed via commit-changes.sh. Designed to be idempotent and safe
|
|
6
|
+
# to re-run: if a PR already exists for the current branch, the script exits 0
|
|
7
|
+
# without creating a duplicate.
|
|
8
|
+
#
|
|
9
|
+
# Degrades gracefully when gh is not installed or the remote is not GitHub — prints
|
|
10
|
+
# manual steps to stderr and exits 0 rather than failing hard.
|
|
11
|
+
#
|
|
12
|
+
# USAGE:
|
|
13
|
+
# create-pr.sh [--json] [--base <branch>] <title> [body]
|
|
14
|
+
#
|
|
15
|
+
# ARGUMENTS:
|
|
16
|
+
# title — PR title (required); Claude supplies this
|
|
17
|
+
# body — PR body/description (optional); defaults to empty string
|
|
18
|
+
#
|
|
19
|
+
# OPTIONS:
|
|
20
|
+
# --base <branch> Base branch for the PR (default: main)
|
|
21
|
+
# --json Emit result as JSON to stdout; all other messages go to stderr
|
|
22
|
+
#
|
|
23
|
+
# OUTPUT — text mode:
|
|
24
|
+
# [created] PR opened (draft): https://github.com/owner/repo/pull/42
|
|
25
|
+
# [exists] PR already open: https://github.com/owner/repo/pull/42
|
|
26
|
+
# [missing] gh not installed — see manual steps above
|
|
27
|
+
# [skipped] Non-GitHub remote — PR creation skipped
|
|
28
|
+
# [error] Branch 'feat-004-create-pr' has no remote tracking ref. Push first.
|
|
29
|
+
#
|
|
30
|
+
# OUTPUT — json mode:
|
|
31
|
+
# {"status": "created", "message": "...", "created": true, "url": "...", "branch": "...", "draft": true}
|
|
32
|
+
# {"status": "exists", "message": "...", "created": false, "url": "...", "branch": "...", "draft": true}
|
|
33
|
+
# {"status": "missing", "message": "...", "created": false, "branch": "..."}
|
|
34
|
+
# {"status": "skipped", "message": "...", "created": false, "branch": "...", "manual_url": "..."}
|
|
35
|
+
# {"status": "error", "message": "..."}
|
|
36
|
+
#
|
|
37
|
+
# EXIT CODES:
|
|
38
|
+
# 0 — PR created, PR already exists, gh missing (graceful), or non-GitHub remote (graceful)
|
|
39
|
+
# 1 — branch not pushed, missing title, git not found, or unexpected error
|
|
40
|
+
|
|
41
|
+
# ---------------------------------------------------------------------------
|
|
42
|
+
# Self-locate and source shared library.
|
|
43
|
+
# ${BASH_SOURCE[0]} is more reliable than $0 when invoked via symlinks or
|
|
44
|
+
# called with an explicit path from a different working directory.
|
|
45
|
+
# ---------------------------------------------------------------------------
|
|
46
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
47
|
+
source "${SCRIPT_DIR}/common.sh"
|
|
48
|
+
# common.sh activates: set -euo pipefail, git_root, output_result,
|
|
49
|
+
# check_prereq, parse_common_args
|
|
50
|
+
|
|
51
|
+
# ---------------------------------------------------------------------------
|
|
52
|
+
# Pass 1: Parse common args.
|
|
53
|
+
# parse_common_args uses a for-loop and does NOT consume "$@", so the full
|
|
54
|
+
# argument list is still available for our own parsing pass below.
|
|
55
|
+
# Sets OUTPUT_FORMAT=json if --json is present.
|
|
56
|
+
# ---------------------------------------------------------------------------
|
|
57
|
+
parse_common_args "$@"
|
|
58
|
+
|
|
59
|
+
# Verify git is available before doing anything else.
|
|
60
|
+
check_prereq git || exit 1
|
|
61
|
+
|
|
62
|
+
# ---------------------------------------------------------------------------
|
|
63
|
+
# Pass 2: Parse script-specific args.
|
|
64
|
+
# Extracts positional args (title, body) and --base. --json is re-encountered
|
|
65
|
+
# here and skipped — it was already handled by parse_common_args above.
|
|
66
|
+
# ---------------------------------------------------------------------------
|
|
67
|
+
title=""
|
|
68
|
+
body=""
|
|
69
|
+
base_branch="main"
|
|
70
|
+
|
|
71
|
+
while [[ $# -gt 0 ]]; do
|
|
72
|
+
case "$1" in
|
|
73
|
+
--json)
|
|
74
|
+
# Already handled by parse_common_args; skip.
|
|
75
|
+
shift
|
|
76
|
+
;;
|
|
77
|
+
--base)
|
|
78
|
+
# Guard against --base with no following value.
|
|
79
|
+
if [[ $# -lt 2 ]]; then
|
|
80
|
+
echo "Error: --base requires a value" >&2
|
|
81
|
+
exit 1
|
|
82
|
+
fi
|
|
83
|
+
base_branch="$2"
|
|
84
|
+
shift 2
|
|
85
|
+
;;
|
|
86
|
+
-*)
|
|
87
|
+
# Unknown flag — silently ignore to stay forward-compatible.
|
|
88
|
+
shift
|
|
89
|
+
;;
|
|
90
|
+
*)
|
|
91
|
+
# Positional argument: first is title, second is body.
|
|
92
|
+
if [[ -z "$title" ]]; then
|
|
93
|
+
title="$1"
|
|
94
|
+
elif [[ -z "$body" ]]; then
|
|
95
|
+
body="$1"
|
|
96
|
+
fi
|
|
97
|
+
shift
|
|
98
|
+
;;
|
|
99
|
+
esac
|
|
100
|
+
done
|
|
101
|
+
|
|
102
|
+
# ---------------------------------------------------------------------------
|
|
103
|
+
# Input validation: require a non-empty PR title.
|
|
104
|
+
# ---------------------------------------------------------------------------
|
|
105
|
+
if [[ -z "$title" ]]; then
|
|
106
|
+
echo "Error: missing required argument <title>" >&2
|
|
107
|
+
echo "Usage: create-pr.sh [--json] [--base <branch>] <title> [body]" >&2
|
|
108
|
+
echo "Example: create-pr.sh \"feat(cli): add --json flag\" \"Adds --json output mode\"" >&2
|
|
109
|
+
exit 1
|
|
110
|
+
fi
|
|
111
|
+
|
|
112
|
+
# ---------------------------------------------------------------------------
|
|
113
|
+
# Capture current branch. Done here (before any conditional exits) so it is
|
|
114
|
+
# available in all output paths below.
|
|
115
|
+
# ---------------------------------------------------------------------------
|
|
116
|
+
current_branch="$(git rev-parse --abbrev-ref HEAD)"
|
|
117
|
+
|
|
118
|
+
# ---------------------------------------------------------------------------
|
|
119
|
+
# gh availability check — graceful degradation.
|
|
120
|
+
# Unlike check_prereq (which exits 1 on failure), we exit 0 here because a
|
|
121
|
+
# missing gh CLI is not an error — the user can create the PR manually.
|
|
122
|
+
# Manual instructions always go to stderr; the machine result goes to stdout.
|
|
123
|
+
# ---------------------------------------------------------------------------
|
|
124
|
+
if ! command -v gh &>/dev/null; then
|
|
125
|
+
echo "gh CLI not found. To create a PR manually:" >&2
|
|
126
|
+
echo " 1. Ensure your branch is pushed: git push -u origin ${current_branch}" >&2
|
|
127
|
+
echo " 2. Open a PR at: https://github.com/<owner>/<repo>/compare/${current_branch}" >&2
|
|
128
|
+
output_result "missing" "gh not installed — see manual steps above" \
|
|
129
|
+
", \"created\": false, \"branch\": \"${current_branch}\""
|
|
130
|
+
exit 0
|
|
131
|
+
fi
|
|
132
|
+
|
|
133
|
+
# ---------------------------------------------------------------------------
|
|
134
|
+
# Remote URL check — non-GitHub detection.
|
|
135
|
+
# gh pr create only works with GitHub-hosted repos. If the origin remote is
|
|
136
|
+
# GitLab, Bitbucket, or anything else, degrade gracefully instead of failing.
|
|
137
|
+
# The || true prevents set -e from killing the script if origin doesn't exist.
|
|
138
|
+
# ---------------------------------------------------------------------------
|
|
139
|
+
remote_url="$(git remote get-url origin 2>/dev/null || true)"
|
|
140
|
+
if [[ "$remote_url" != *github.com* ]]; then
|
|
141
|
+
echo "Remote is not GitHub (${remote_url:-no origin remote found})." >&2
|
|
142
|
+
echo "To create a PR manually, visit your hosting provider and open a PR for: ${current_branch}" >&2
|
|
143
|
+
output_result "skipped" "Non-GitHub remote — PR creation skipped" \
|
|
144
|
+
", \"created\": false, \"branch\": \"${current_branch}\", \"manual_url\": \"${remote_url:-}\""
|
|
145
|
+
exit 0
|
|
146
|
+
fi
|
|
147
|
+
|
|
148
|
+
# ---------------------------------------------------------------------------
|
|
149
|
+
# Branch-pushed check — hard error (exit 1).
|
|
150
|
+
# If the branch has no remote tracking ref, there is nothing gh can PR against.
|
|
151
|
+
# This is a real error state, not a graceful-degradation case — the user must
|
|
152
|
+
# push the branch first (commit-changes.sh handles this automatically).
|
|
153
|
+
# ---------------------------------------------------------------------------
|
|
154
|
+
if ! git rev-parse --abbrev-ref --symbolic-full-name "@{u}" &>/dev/null; then
|
|
155
|
+
output_result "error" \
|
|
156
|
+
"Branch '${current_branch}' has no remote tracking ref. Push the branch first (commit-changes.sh handles this)."
|
|
157
|
+
exit 1
|
|
158
|
+
fi
|
|
159
|
+
|
|
160
|
+
# ---------------------------------------------------------------------------
|
|
161
|
+
# Idempotency check — detect existing open PR before creating.
|
|
162
|
+
# gh pr list --json url --jq '.[0].url' extracts the URL of the first result.
|
|
163
|
+
# The || true prevents set -e from aborting if gh returns non-zero (e.g. no
|
|
164
|
+
# open PRs found for this branch returns an empty list, not an error, but
|
|
165
|
+
# older gh versions may exit non-zero in edge cases).
|
|
166
|
+
# ---------------------------------------------------------------------------
|
|
167
|
+
existing_url="$(gh pr list --head "${current_branch}" --state open --json url --jq '.[0].url' 2>/dev/null || true)"
|
|
168
|
+
if [[ -n "$existing_url" ]]; then
|
|
169
|
+
output_result "exists" "PR already open: ${existing_url}" \
|
|
170
|
+
", \"created\": false, \"url\": \"${existing_url}\", \"branch\": \"${current_branch}\", \"draft\": true"
|
|
171
|
+
exit 0
|
|
172
|
+
fi
|
|
173
|
+
|
|
174
|
+
# ---------------------------------------------------------------------------
|
|
175
|
+
# Create the draft PR.
|
|
176
|
+
# gh pr create prints the new PR URL to stdout; we capture it directly.
|
|
177
|
+
# Diagnostic progress messages from gh go to stderr automatically.
|
|
178
|
+
# set -euo pipefail (inherited from common.sh) ensures any failure here exits
|
|
179
|
+
# immediately rather than silently continuing with an empty pr_url.
|
|
180
|
+
# ---------------------------------------------------------------------------
|
|
181
|
+
pr_url="$(gh pr create --draft --title "${title}" --body "${body:-}" --base "${base_branch}")"
|
|
182
|
+
|
|
183
|
+
output_result "created" "PR opened (draft): ${pr_url}" \
|
|
184
|
+
", \"created\": true, \"url\": \"${pr_url}\", \"branch\": \"${current_branch}\", \"draft\": true"
|
|
@@ -124,11 +124,30 @@ All [N] tasks implemented.
|
|
|
124
124
|
|
|
125
125
|
### Files changed
|
|
126
126
|
[List of files created or modified]
|
|
127
|
-
|
|
128
|
-
Archive this feature? (moves to archive/ and removes from active features)
|
|
129
127
|
```
|
|
130
128
|
|
|
131
|
-
2.
|
|
129
|
+
2. **Git Automation Checkpoint:**
|
|
130
|
+
Check if `scripts/bash/commit-changes.sh` exists (use Glob: `scripts/bash/commit-changes.sh`):
|
|
131
|
+
|
|
132
|
+
**If found:**
|
|
133
|
+
- Ask the user: "All [N] tasks are complete. Please confirm your tests pass before I commit. (yes/no)"
|
|
134
|
+
- If **yes**:
|
|
135
|
+
- Compose a Conventional Commits message: `feat(<feature-name>): <brief description from spec Purpose>`
|
|
136
|
+
- Stage implementation files: run `git add <file> ...` for each file created or modified during this implementation (review the progress log in `status.md` for the file list)
|
|
137
|
+
- Run: `bash scripts/bash/commit-changes.sh --json "<commit_message>"`
|
|
138
|
+
- On `status: "ok"`: report "Committed [sha]." and print "**To undo: `git reset HEAD~1`**"
|
|
139
|
+
Then ask: "Would you like to open a draft PR? (yes/no)"
|
|
140
|
+
- If **yes** and `scripts/bash/create-pr.sh` exists:
|
|
141
|
+
Generate a PR title (same as the commit message description) and a short body summarising what was built, then run: `bash scripts/bash/create-pr.sh --json "<title>" "<body>"`
|
|
142
|
+
Handle all statuses: `created` → report URL; `exists` → report existing URL; `missing` → display the manual steps printed to stderr; `skipped` → note non-GitHub remote; `error` → report error
|
|
143
|
+
- If `create-pr.sh` not found: note "Run `npx mema-kit` to install PR automation scripts."
|
|
144
|
+
- On `status: "error"`: report the error; do NOT proceed to archiving
|
|
145
|
+
- If **no**: note "Skipping commit — remember to run `git add` + `git commit` + `git push` manually before opening a PR."
|
|
146
|
+
|
|
147
|
+
**If not found:** note: "Git scripts not installed — run `npx mema-kit` to enable automatic commit and PR creation."
|
|
148
|
+
|
|
149
|
+
3. Archive this feature? (moves to `archive/` and removes from active features)
|
|
150
|
+
If user confirms, archive in Phase 3.
|
|
132
151
|
|
|
133
152
|
### 2h: Learn
|
|
134
153
|
|
|
@@ -96,10 +96,24 @@ Write `.mema/features/NNN-name/spec.md`:
|
|
|
96
96
|
[What this feature deliberately does NOT include]
|
|
97
97
|
```
|
|
98
98
|
|
|
99
|
+
### Create Feature Branch (if scripts available)
|
|
100
|
+
|
|
101
|
+
After saving `spec.md`, create (or check out) the feature branch:
|
|
102
|
+
|
|
103
|
+
1. Parse the feature number and name from the feature directory path:
|
|
104
|
+
- Example: `features/005-skill-integration/` → NNN = `005`, name = `skill-integration`
|
|
105
|
+
2. Check if `scripts/bash/create-feature-branch.sh` exists (use Glob: `scripts/bash/create-feature-branch.sh`)
|
|
106
|
+
3. **If found:** run: `bash scripts/bash/create-feature-branch.sh <NNN> <name> --json`
|
|
107
|
+
- `status: "created"` → include in confirmation: "Branch created: feat-NNN-name"
|
|
108
|
+
- `status: "exists"` → include: "Branch already exists: feat-NNN-name (checked out)"
|
|
109
|
+
- `status: "error"` → surface the error message; note that spec.md was saved successfully; tell user to resolve the issue (e.g. commit or stash dirty files) and re-run `/mema.specify`
|
|
110
|
+
4. **If not found:** note: "Git scripts not installed — run `npx mema-kit` to enable automatic branch creation."
|
|
111
|
+
|
|
99
112
|
### Confirm to User
|
|
100
113
|
|
|
101
114
|
```
|
|
102
115
|
Spec saved: features/[NNN-name]/spec.md
|
|
116
|
+
Branch: [feat-NNN-name (created) | feat-NNN-name (exists) | error — see above | scripts not installed]
|
|
103
117
|
|
|
104
118
|
[Feature name]: [one-line purpose summary]
|
|
105
119
|
[N] acceptance criteria defined
|