hub-launch 1.15.0 → 1.17.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/CHANGELOG.md +12 -0
- package/README.md +103 -2
- package/dist/commands/init.d.ts +55 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +93 -103
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/instructions.d.ts +17 -0
- package/dist/commands/instructions.d.ts.map +1 -0
- package/dist/commands/instructions.js +51 -0
- package/dist/commands/instructions.js.map +1 -0
- package/dist/commands/launch.d.ts +21 -0
- package/dist/commands/launch.d.ts.map +1 -1
- package/dist/commands/launch.js +61 -0
- package/dist/commands/launch.js.map +1 -1
- package/dist/commands/script.d.ts +28 -0
- package/dist/commands/script.d.ts.map +1 -0
- package/dist/commands/script.js +69 -0
- package/dist/commands/script.js.map +1 -0
- package/dist/commands/session-hook.d.ts +26 -0
- package/dist/commands/session-hook.d.ts.map +1 -0
- package/dist/commands/session-hook.js +146 -0
- package/dist/commands/session-hook.js.map +1 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -1
- package/dist/scripts/fix-commit.d.ts +2 -0
- package/dist/scripts/fix-commit.d.ts.map +1 -0
- package/dist/scripts/fix-commit.js +125 -0
- package/dist/scripts/fix-commit.js.map +1 -0
- package/dist/scripts/fix-setup.d.ts +2 -0
- package/dist/scripts/fix-setup.d.ts.map +1 -0
- package/dist/scripts/fix-setup.js +161 -0
- package/dist/scripts/fix-setup.js.map +1 -0
- package/dist/scripts/launch-run.d.ts +14 -0
- package/dist/scripts/launch-run.d.ts.map +1 -0
- package/dist/scripts/launch-run.js +117 -0
- package/dist/scripts/launch-run.js.map +1 -0
- package/dist/scripts/lib/exec.d.ts +48 -0
- package/dist/scripts/lib/exec.d.ts.map +1 -0
- package/dist/scripts/lib/exec.js +61 -0
- package/dist/scripts/lib/exec.js.map +1 -0
- package/dist/scripts/lib/io.d.ts +48 -0
- package/dist/scripts/lib/io.d.ts.map +1 -0
- package/dist/scripts/lib/io.js +72 -0
- package/dist/scripts/lib/io.js.map +1 -0
- package/dist/scripts/lib/read-config.d.ts +26 -0
- package/dist/scripts/lib/read-config.d.ts.map +1 -0
- package/dist/scripts/lib/read-config.js +65 -0
- package/dist/scripts/lib/read-config.js.map +1 -0
- package/dist/scripts/merge-local.d.ts +2 -0
- package/dist/scripts/merge-local.d.ts.map +1 -0
- package/dist/scripts/merge-local.js +146 -0
- package/dist/scripts/merge-local.js.map +1 -0
- package/dist/scripts/merge-remote.d.ts +12 -0
- package/dist/scripts/merge-remote.d.ts.map +1 -0
- package/dist/scripts/merge-remote.js +71 -0
- package/dist/scripts/merge-remote.js.map +1 -0
- package/dist/scripts/schedule-manage.d.ts +2 -0
- package/dist/scripts/schedule-manage.d.ts.map +1 -0
- package/dist/scripts/schedule-manage.js +151 -0
- package/dist/scripts/schedule-manage.js.map +1 -0
- package/dist/scripts/schedule-run.d.ts +2 -0
- package/dist/scripts/schedule-run.d.ts.map +1 -0
- package/dist/scripts/schedule-run.js +146 -0
- package/dist/scripts/schedule-run.js.map +1 -0
- package/dist/scripts/verify-gather.d.ts +2 -0
- package/dist/scripts/verify-gather.d.ts.map +1 -0
- package/dist/scripts/verify-gather.js +210 -0
- package/dist/scripts/verify-gather.js.map +1 -0
- package/dist/scripts/verify-post.d.ts +12 -0
- package/dist/scripts/verify-post.d.ts.map +1 -0
- package/dist/scripts/verify-post.js +72 -0
- package/dist/scripts/verify-post.js.map +1 -0
- package/dist/services/git/FilePublishService.d.ts +6 -0
- package/dist/services/git/FilePublishService.d.ts.map +1 -1
- package/dist/services/git/FilePublishService.js +79 -2
- package/dist/services/git/FilePublishService.js.map +1 -1
- package/dist/services/git/WorktreeService.d.ts +17 -0
- package/dist/services/git/WorktreeService.d.ts.map +1 -1
- package/dist/services/git/WorktreeService.js +40 -0
- package/dist/services/git/WorktreeService.js.map +1 -1
- package/dist/templates/planning-instructions.md +7 -7
- package/dist/templates/skill-creation-instructions.md +2 -2
- package/dist/templates/skills/hula-confirm/SKILL.md +3 -3
- package/dist/templates/skills/hula-fix/SKILL.md +3 -3
- package/dist/templates/skills/hula-launch/SKILL.md +14 -8
- package/dist/templates/skills/hula-merge/SKILL.md +2 -2
- package/dist/templates/skills/hula-plan/SKILL.md +5 -5
- package/dist/templates/skills/hula-schedule/SKILL.md +16 -16
- package/dist/templates/skills/hula-verify/SKILL.md +2 -2
- package/dist/types/config.schema.d.ts +235 -0
- package/dist/types/config.schema.d.ts.map +1 -1
- package/dist/types/config.schema.js +66 -0
- package/dist/types/config.schema.js.map +1 -1
- package/dist/utils/client-session.d.ts +2 -2
- package/dist/utils/client-session.js +2 -2
- package/package.json +6 -5
- package/scripts/postinstall.mjs +101 -0
- package/dist/templates/scripts/hula-fix-commit.sh +0 -114
- package/dist/templates/scripts/hula-fix-setup.sh +0 -152
- package/dist/templates/scripts/hula-launch-run.sh +0 -140
- package/dist/templates/scripts/hula-merge-local.sh +0 -138
- package/dist/templates/scripts/hula-merge-remote.sh +0 -94
- package/dist/templates/scripts/hula-read-config.sh +0 -38
- package/dist/templates/scripts/hula-schedule-manage.sh +0 -159
- package/dist/templates/scripts/hula-schedule-run.sh +0 -148
- package/dist/templates/scripts/hula-session-hook.sh +0 -68
- package/dist/templates/scripts/hula-verify-gather.sh +0 -135
- package/dist/templates/scripts/hula-verify-post.sh +0 -84
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* postinstall: generate global Claude Code slash commands for hula.
|
|
4
|
+
*
|
|
5
|
+
* Writes ~/.claude/commands/<skill>.md for each hula Agent Skill, sourcing the
|
|
6
|
+
* command body from the package's bundled SKILL.md files. This makes /hula-plan,
|
|
7
|
+
* /hula-fix, etc. available in EVERY project's Claude Code session with zero
|
|
8
|
+
* per-project footprint — the "install hula → skills appear" model, no
|
|
9
|
+
* marketplace/plugin required.
|
|
10
|
+
*
|
|
11
|
+
* Design constraints:
|
|
12
|
+
* - Self-contained (no imports from dist/) so it works even when this runs
|
|
13
|
+
* before the TypeScript build (e.g. `pnpm install` in the repo).
|
|
14
|
+
* - Never throws: a postinstall failure must not break `npm install`. Any error
|
|
15
|
+
* is logged and swallowed with exit 0.
|
|
16
|
+
* - Idempotent: re-running (e.g. on `hula` upgrade) overwrites the same files,
|
|
17
|
+
* refreshing them to the installed version.
|
|
18
|
+
*
|
|
19
|
+
* Source of truth for the skill set: the AGENT_SKILLS list in
|
|
20
|
+
* src/commands/init.ts. Keep SKILL_NAMES below in sync with it.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { homedir } from 'node:os';
|
|
24
|
+
import {
|
|
25
|
+
existsSync,
|
|
26
|
+
mkdirSync,
|
|
27
|
+
readFileSync,
|
|
28
|
+
writeFileSync,
|
|
29
|
+
} from 'node:fs';
|
|
30
|
+
import { dirname, join } from 'node:path';
|
|
31
|
+
import { fileURLToPath } from 'node:url';
|
|
32
|
+
|
|
33
|
+
/** Skill directories that become global Claude Code slash commands.
|
|
34
|
+
* Mirrors AGENT_SKILLS in src/commands/init.ts. */
|
|
35
|
+
const SKILL_NAMES = [
|
|
36
|
+
'hula-plan',
|
|
37
|
+
'hula-confirm',
|
|
38
|
+
'hula-fix',
|
|
39
|
+
'hula-create',
|
|
40
|
+
'hula-verify',
|
|
41
|
+
'hula-merge',
|
|
42
|
+
'hula-launch',
|
|
43
|
+
'hula-schedule',
|
|
44
|
+
'hula-upload',
|
|
45
|
+
'hula-info',
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
function main() {
|
|
49
|
+
// Opt-out escape hatch for CI / sandboxed installs.
|
|
50
|
+
if (process.env.HULA_SKIP_CLAUDE_COMMANDS === '1') {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
55
|
+
const packageRoot = join(here, '..');
|
|
56
|
+
|
|
57
|
+
// Prefer the built templates (shipped in the published package); fall back to
|
|
58
|
+
// source (local dev install before `npm run build`).
|
|
59
|
+
const candidateSkillDirs = [
|
|
60
|
+
join(packageRoot, 'dist', 'templates', 'skills'),
|
|
61
|
+
join(packageRoot, 'src', 'templates', 'skills'),
|
|
62
|
+
];
|
|
63
|
+
const skillsDir = candidateSkillDirs.find((d) => existsSync(d));
|
|
64
|
+
if (!skillsDir) {
|
|
65
|
+
// Nothing to install yet (e.g. pre-build). Silent no-op; a later build/run
|
|
66
|
+
// will regenerate. Not an error.
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const commandsDir = join(homedir(), '.claude', 'commands');
|
|
71
|
+
mkdirSync(commandsDir, { recursive: true });
|
|
72
|
+
|
|
73
|
+
let written = 0;
|
|
74
|
+
for (const name of SKILL_NAMES) {
|
|
75
|
+
const src = join(skillsDir, name, 'SKILL.md');
|
|
76
|
+
if (!existsSync(src)) continue;
|
|
77
|
+
try {
|
|
78
|
+
const body = readFileSync(src, 'utf8');
|
|
79
|
+
writeFileSync(join(commandsDir, `${name}.md`), body, 'utf8');
|
|
80
|
+
written += 1;
|
|
81
|
+
} catch {
|
|
82
|
+
// Skip this one; keep going with the rest.
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (written > 0) {
|
|
87
|
+
console.log(
|
|
88
|
+
`hula: installed ${written} Claude Code command(s) to ~/.claude/commands/ (use /hula-plan, /hula-fix, …)`,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
main();
|
|
95
|
+
} catch (err) {
|
|
96
|
+
// Never fail the install.
|
|
97
|
+
console.warn(
|
|
98
|
+
`hula: could not install global Claude Code commands (${err && err.message ? err.message : err}). ` +
|
|
99
|
+
'Run `hula init --with-claude-commands` for repo-local commands instead.',
|
|
100
|
+
);
|
|
101
|
+
}
|
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# hula-fix-commit.sh — Step 9 of the hula.fix workflow
|
|
3
|
-
# Usage: bash .github/scripts/hula-fix-commit.sh <issue-number> <commit-message> [--worktree-path <path>]
|
|
4
|
-
#
|
|
5
|
-
# Performs: git add -A → git commit → git push from within the fix worktree
|
|
6
|
-
# Outputs structured JSON to stdout; all other output goes to stderr
|
|
7
|
-
# Exit codes: 0=success, 1=user error, 2=tool error
|
|
8
|
-
|
|
9
|
-
set -euo pipefail
|
|
10
|
-
|
|
11
|
-
# ── helpers ────────────────────────────────────────────────────────────────────
|
|
12
|
-
|
|
13
|
-
json_escape() {
|
|
14
|
-
printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g; s/$/\\n/g' | tr -d '\n' | sed 's/\\n$//'
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
die() {
|
|
18
|
-
local code="$1"; shift
|
|
19
|
-
local safe_msg
|
|
20
|
-
safe_msg=$(json_escape "$*")
|
|
21
|
-
printf '{"status":"error","code":%d,"message":"%s"}\n' "$code" "$safe_msg" >&1
|
|
22
|
-
exit "$code"
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
# ── argument parsing ────────────────────────────────────────────────────────────
|
|
26
|
-
|
|
27
|
-
ISSUE_NUMBER=''
|
|
28
|
-
COMMIT_MSG=''
|
|
29
|
-
WORKTREE_PATH=''
|
|
30
|
-
|
|
31
|
-
while [[ $# -gt 0 ]]; do
|
|
32
|
-
case "$1" in
|
|
33
|
-
--worktree-path)
|
|
34
|
-
WORKTREE_PATH="$2"; shift 2 ;;
|
|
35
|
-
--worktree-path=*)
|
|
36
|
-
WORKTREE_PATH="${1#--worktree-path=}"; shift ;;
|
|
37
|
-
*)
|
|
38
|
-
if [[ -z "$ISSUE_NUMBER" ]]; then
|
|
39
|
-
ISSUE_NUMBER="$1"
|
|
40
|
-
elif [[ -z "$COMMIT_MSG" ]]; then
|
|
41
|
-
COMMIT_MSG="$1"
|
|
42
|
-
else
|
|
43
|
-
die 1 "Unexpected argument: $1"
|
|
44
|
-
fi
|
|
45
|
-
shift ;;
|
|
46
|
-
esac
|
|
47
|
-
done
|
|
48
|
-
|
|
49
|
-
if [[ -z "$ISSUE_NUMBER" ]]; then
|
|
50
|
-
die 1 "Usage: bash .github/scripts/hula-fix-commit.sh <issue-number> <commit-message> [--worktree-path <path>]"
|
|
51
|
-
fi
|
|
52
|
-
|
|
53
|
-
if [[ -z "$COMMIT_MSG" ]]; then
|
|
54
|
-
die 1 "Commit message is required."
|
|
55
|
-
fi
|
|
56
|
-
|
|
57
|
-
# ── Resolve worktree path ─────────────────────────────────────────────────────
|
|
58
|
-
|
|
59
|
-
if [[ -z "$WORKTREE_PATH" ]]; then
|
|
60
|
-
# Try to read from session file (use absolute path so it works from any CWD)
|
|
61
|
-
REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null) || REPO_ROOT="$(pwd)"
|
|
62
|
-
SESSION_FILE="${REPO_ROOT}/.hublaunch/.fix-sessions/issue-${ISSUE_NUMBER}.json"
|
|
63
|
-
if [[ -f "$SESSION_FILE" ]]; then
|
|
64
|
-
# Extract worktreePath from JSON (handles both compact and pretty-printed formats).
|
|
65
|
-
# Pattern .*"worktreePath" *: *"\([^"]*\)".* tolerates optional whitespace, matching:
|
|
66
|
-
# - Compact: "worktreePath":"..."
|
|
67
|
-
# - Pretty: "worktreePath": "..."
|
|
68
|
-
WORKTREE_PATH=$(grep '"worktreePath"' "$SESSION_FILE" | sed 's/.*"worktreePath" *: *"\([^"]*\)".*/\1/')
|
|
69
|
-
fi
|
|
70
|
-
fi
|
|
71
|
-
|
|
72
|
-
if [[ -z "$WORKTREE_PATH" ]]; then
|
|
73
|
-
# Last resort: infer from repo name using config
|
|
74
|
-
REPO_NAME=$(gh repo view --json name -q '.name' 2>/dev/null) || die 2 "Failed to determine repo name."
|
|
75
|
-
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
76
|
-
source "${SCRIPT_DIR}/hula-read-config.sh"
|
|
77
|
-
WORKTREE_PATH="${HULA_WORKTREE_BASE_PATH}/hula/${REPO_NAME}/fix-${ISSUE_NUMBER}"
|
|
78
|
-
fi
|
|
79
|
-
|
|
80
|
-
if [[ ! -d "$WORKTREE_PATH" ]]; then
|
|
81
|
-
die 1 "Worktree not found at ${WORKTREE_PATH}. Run hula-fix-setup.sh first."
|
|
82
|
-
fi
|
|
83
|
-
|
|
84
|
-
# ── Check for changes ─────────────────────────────────────────────────────────
|
|
85
|
-
|
|
86
|
-
cd "$WORKTREE_PATH"
|
|
87
|
-
|
|
88
|
-
PORCELAIN=$(git status --porcelain 2>/dev/null)
|
|
89
|
-
|
|
90
|
-
if [[ -z "$PORCELAIN" ]]; then
|
|
91
|
-
printf '{"status":"success","message":"Nothing to commit","filesChanged":0,"worktreePath":"%s"}\n' \
|
|
92
|
-
"$(json_escape "$WORKTREE_PATH")"
|
|
93
|
-
exit 0
|
|
94
|
-
fi
|
|
95
|
-
|
|
96
|
-
FILES_CHANGED=$(printf '%s' "$PORCELAIN" | grep -c '' || true)
|
|
97
|
-
|
|
98
|
-
# ── Stage, commit, push ───────────────────────────────────────────────────────
|
|
99
|
-
|
|
100
|
-
git add -A >&2 2>&1 || die 2 "git add failed"
|
|
101
|
-
|
|
102
|
-
git commit -m "$COMMIT_MSG" >&2 2>&1 \
|
|
103
|
-
|| die 2 "Failed to commit changes. Ensure git user.name and user.email are configured."
|
|
104
|
-
|
|
105
|
-
git push >&2 2>&1 \
|
|
106
|
-
|| die 2 "Push failed. Try: cd ${WORKTREE_PATH} && git push"
|
|
107
|
-
|
|
108
|
-
# ── Output result ─────────────────────────────────────────────────────────────
|
|
109
|
-
|
|
110
|
-
SAFE_MSG=$(json_escape "$COMMIT_MSG")
|
|
111
|
-
SAFE_PATH=$(json_escape "$WORKTREE_PATH")
|
|
112
|
-
|
|
113
|
-
printf '{"status":"success","commitMessage":"%s","filesChanged":%d,"worktreePath":"%s"}\n' \
|
|
114
|
-
"$SAFE_MSG" "$FILES_CHANGED" "$SAFE_PATH"
|
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# hula-fix-setup.sh — Steps 2-6 of the hula.fix workflow
|
|
3
|
-
# Usage: bash .github/scripts/hula-fix-setup.sh <issue-number> [--repo-root <path>]
|
|
4
|
-
#
|
|
5
|
-
# Performs: issue lookup → worktree creation → session file writing
|
|
6
|
-
# Outputs structured JSON to stdout; all other output goes to stderr
|
|
7
|
-
# Exit codes: 0=success, 1=user error, 2=tool error
|
|
8
|
-
|
|
9
|
-
set -euo pipefail
|
|
10
|
-
|
|
11
|
-
# ── helpers ────────────────────────────────────────────────────────────────────
|
|
12
|
-
|
|
13
|
-
json_escape() {
|
|
14
|
-
# Escape a string for safe embedding inside a JSON double-quoted value
|
|
15
|
-
printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g; s/$/\\n/g' | tr -d '\n' | sed 's/\\n$//'
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
die() {
|
|
19
|
-
local code="$1"; shift
|
|
20
|
-
local safe_msg
|
|
21
|
-
safe_msg=$(json_escape "$*")
|
|
22
|
-
printf '{"status":"error","code":%d,"message":"%s"}\n' "$code" "$safe_msg" >&1
|
|
23
|
-
exit "$code"
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
emit_json() {
|
|
27
|
-
# emit_json key value [key value ...]
|
|
28
|
-
# Outputs compact single-line JSON: {"key":"value",...}
|
|
29
|
-
# All values must already be JSON-safe (escaped via json_escape).
|
|
30
|
-
local out='{'
|
|
31
|
-
local sep=''
|
|
32
|
-
while [[ $# -ge 2 ]]; do
|
|
33
|
-
local k="$1" v="$2"; shift 2
|
|
34
|
-
out="${out}${sep}\"${k}\":\"${v}\""
|
|
35
|
-
sep=','
|
|
36
|
-
done
|
|
37
|
-
printf '%s}\n' "$out"
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
# ── argument parsing ────────────────────────────────────────────────────────────
|
|
41
|
-
|
|
42
|
-
ISSUE_NUMBER=''
|
|
43
|
-
REPO_ROOT=''
|
|
44
|
-
|
|
45
|
-
while [[ $# -gt 0 ]]; do
|
|
46
|
-
case "$1" in
|
|
47
|
-
--repo-root)
|
|
48
|
-
REPO_ROOT="$2"; shift 2 ;;
|
|
49
|
-
--repo-root=*)
|
|
50
|
-
REPO_ROOT="${1#--repo-root=}"; shift ;;
|
|
51
|
-
[0-9]*)
|
|
52
|
-
ISSUE_NUMBER="$1"; shift ;;
|
|
53
|
-
*)
|
|
54
|
-
die 1 "Unknown argument: $1" ;;
|
|
55
|
-
esac
|
|
56
|
-
done
|
|
57
|
-
|
|
58
|
-
if [[ -z "$ISSUE_NUMBER" ]]; then
|
|
59
|
-
die 1 "Usage: bash .github/scripts/hula-fix-setup.sh <issue-number> [--repo-root <path>]"
|
|
60
|
-
fi
|
|
61
|
-
|
|
62
|
-
if [[ -n "$REPO_ROOT" ]]; then
|
|
63
|
-
cd "$REPO_ROOT"
|
|
64
|
-
fi
|
|
65
|
-
|
|
66
|
-
# ── Step 1: Infer owner/repo and repo name from git remote ─────────────────────
|
|
67
|
-
|
|
68
|
-
NAME_WITH_OWNER=$(gh repo view --json nameWithOwner -q '.nameWithOwner' 2>/dev/null) \
|
|
69
|
-
|| die 2 "Failed to determine repo from git remote. Is this a GitHub repo?"
|
|
70
|
-
|
|
71
|
-
REPO_NAME=$(gh repo view --json name -q '.name' 2>/dev/null) \
|
|
72
|
-
|| die 2 "Failed to determine repo name."
|
|
73
|
-
|
|
74
|
-
# ── Step 2: Fetch issue details ────────────────────────────────────────────────
|
|
75
|
-
|
|
76
|
-
ISSUE_JSON=$(gh issue view "$ISSUE_NUMBER" --json title,body,labels 2>/dev/null) \
|
|
77
|
-
|| die 1 "Issue #${ISSUE_NUMBER} not found or not accessible."
|
|
78
|
-
|
|
79
|
-
ISSUE_TITLE=$(printf '%s' "$ISSUE_JSON" | grep -o '"title":"[^"]*"' | head -1 | sed 's/"title":"//;s/"$//')
|
|
80
|
-
|
|
81
|
-
# ── Step 3: Validate issue is tracked and has a PR (via hula view) ─────────────
|
|
82
|
-
|
|
83
|
-
HULA_VIEW=$(hula view 2>&1) || true
|
|
84
|
-
|
|
85
|
-
if printf '%s' "$HULA_VIEW" | grep -qi "not tracked\|no issues"; then
|
|
86
|
-
die 1 "Issue #${ISSUE_NUMBER} is not tracked. Run: hula track ${ISSUE_NUMBER}"
|
|
87
|
-
fi
|
|
88
|
-
|
|
89
|
-
# ── Step 4: Find PR branch for the issue ──────────────────────────────────────
|
|
90
|
-
|
|
91
|
-
PR_BRANCH=$(gh pr list --search "linked:${ISSUE_NUMBER}" --json headRefName -q '.[0].headRefName' 2>/dev/null) || true
|
|
92
|
-
|
|
93
|
-
if [[ -z "$PR_BRANCH" ]]; then
|
|
94
|
-
# Fallback: search PR bodies for issue references
|
|
95
|
-
# Handles both short format (#N) and full-path format (owner/repo#N)
|
|
96
|
-
# Uses gh's built-in jq (-q) — no external jq required
|
|
97
|
-
JQ_EXPR='.[] | select(.body != null) | select(.body | test("(Fixes|Closes|Resolves)[ ]+([A-Za-z0-9._-]+/[A-Za-z0-9._-]+)?#'"${ISSUE_NUMBER}"'([^0-9]|$)|(^|[^0-9])#'"${ISSUE_NUMBER}"'([^0-9]|$)"; "i")) | .headRefName'
|
|
98
|
-
PR_BRANCH=$(gh pr list --json headRefName,body --limit 100 -q "$JQ_EXPR" 2>/dev/null | head -1) || true
|
|
99
|
-
fi
|
|
100
|
-
|
|
101
|
-
if [[ -z "$PR_BRANCH" ]]; then
|
|
102
|
-
die 1 "No PR found for issue #${ISSUE_NUMBER}. Create a PR first with: hula checkout ${ISSUE_NUMBER}"
|
|
103
|
-
fi
|
|
104
|
-
|
|
105
|
-
# ── Step 5: Determine worktree path ───────────────────────────────────────────
|
|
106
|
-
|
|
107
|
-
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
108
|
-
source "${SCRIPT_DIR}/hula-read-config.sh"
|
|
109
|
-
WORKTREE_PATH="${HULA_WORKTREE_BASE_PATH}/hula/${REPO_NAME}/fix-${ISSUE_NUMBER}"
|
|
110
|
-
|
|
111
|
-
# ── Step 6: Create or reuse the worktree ─────────────────────────────────────
|
|
112
|
-
|
|
113
|
-
WORKTREE_STATUS='reused'
|
|
114
|
-
|
|
115
|
-
if git worktree list --porcelain 2>/dev/null | grep -qxF "worktree ${WORKTREE_PATH}"; then
|
|
116
|
-
printf 'ℹ️ Resuming existing fix worktree at %s\n' "$WORKTREE_PATH" >&2
|
|
117
|
-
else
|
|
118
|
-
printf '🌳 Creating fix worktree at: %s\n' "$WORKTREE_PATH" >&2
|
|
119
|
-
git fetch origin >&2 2>&1 || die 2 "git fetch origin failed"
|
|
120
|
-
git worktree add "$WORKTREE_PATH" "$PR_BRANCH" >&2 2>&1 \
|
|
121
|
-
|| die 2 "Failed to create worktree at ${WORKTREE_PATH}. Check that branch ${PR_BRANCH} exists."
|
|
122
|
-
WORKTREE_STATUS='created'
|
|
123
|
-
fi
|
|
124
|
-
|
|
125
|
-
# ── Step 7: Write session file ────────────────────────────────────────────────
|
|
126
|
-
|
|
127
|
-
REPO_ROOT_SESSION=$(git rev-parse --show-toplevel 2>/dev/null) || REPO_ROOT_SESSION="$(pwd)"
|
|
128
|
-
SESSION_DIR="${REPO_ROOT_SESSION}/.hublaunch/.fix-sessions"
|
|
129
|
-
mkdir -p "$SESSION_DIR"
|
|
130
|
-
|
|
131
|
-
STARTED_AT=$(date -u +"%Y-%m-%dT%H:%M:%S.000Z" 2>/dev/null || date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
132
|
-
|
|
133
|
-
SESSION_FILE="${SESSION_DIR}/issue-${ISSUE_NUMBER}.json"
|
|
134
|
-
SAFE_WT_SESSION=$(json_escape "$WORKTREE_PATH")
|
|
135
|
-
SAFE_BR_SESSION=$(json_escape "$PR_BRANCH")
|
|
136
|
-
# Write compact single-line JSON via emit_json so the session file format matches
|
|
137
|
-
# what readers (hula-fix-commit.sh) expect. Avoids the pretty-printed/compact mismatch.
|
|
138
|
-
emit_json \
|
|
139
|
-
"issueNumber" "$ISSUE_NUMBER" \
|
|
140
|
-
"worktreePath" "$SAFE_WT_SESSION" \
|
|
141
|
-
"prBranch" "$SAFE_BR_SESSION" \
|
|
142
|
-
"startedAt" "$STARTED_AT" > "$SESSION_FILE"
|
|
143
|
-
|
|
144
|
-
# ── Output result ─────────────────────────────────────────────────────────────
|
|
145
|
-
|
|
146
|
-
SAFE_TITLE=$(json_escape "$ISSUE_TITLE")
|
|
147
|
-
SAFE_BRANCH=$(json_escape "$PR_BRANCH")
|
|
148
|
-
SAFE_WORKTREE=$(json_escape "$WORKTREE_PATH")
|
|
149
|
-
SAFE_SESSION=$(json_escape "$SESSION_FILE")
|
|
150
|
-
|
|
151
|
-
printf '{"status":"success","issueNumber":%s,"issueTitle":"%s","prBranch":"%s","worktreePath":"%s","sessionFile":"%s","worktreeStatus":"%s"}\n' \
|
|
152
|
-
"$ISSUE_NUMBER" "$SAFE_TITLE" "$SAFE_BRANCH" "$SAFE_WORKTREE" "$SAFE_SESSION" "$WORKTREE_STATUS"
|
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# hula-launch-run.sh — Step 4 of the hula.launch workflow
|
|
3
|
-
# Usage: bash .github/scripts/hula-launch-run.sh <plan-path> <branch-name> [--handoff <username>] [--test] [--kill] [--kill-and-relaunch]
|
|
4
|
-
# bash .github/scripts/hula-launch-run.sh <branch-name> --kill # cancel only; no plan path required
|
|
5
|
-
#
|
|
6
|
-
# Performs: hula launch (which handles plan upload automatically)
|
|
7
|
-
# Outputs structured JSON to stdout; all other output goes to stderr.
|
|
8
|
-
# Exit codes: 0=success, 1=user error, 2=tool error
|
|
9
|
-
|
|
10
|
-
set -euo pipefail
|
|
11
|
-
|
|
12
|
-
# ── helpers ────────────────────────────────────────────────────────────────────
|
|
13
|
-
|
|
14
|
-
json_escape() {
|
|
15
|
-
if command -v jq &>/dev/null; then
|
|
16
|
-
# jq -Rs . reads stdin as a raw string and outputs a JSON string (with surrounding quotes).
|
|
17
|
-
# Strip the surrounding quotes; the escaped content is safe to embed in JSON.
|
|
18
|
-
printf '%s' "$1" | jq -Rs . | sed 's/^"//;s/"$//'
|
|
19
|
-
else
|
|
20
|
-
# Fallback: escape backslashes and double quotes, collapse control chars to spaces
|
|
21
|
-
printf '%s' "$1" \
|
|
22
|
-
| sed 's/\\/\\\\/g; s/"/\\"/g' \
|
|
23
|
-
| tr '\n\r\t' ' '
|
|
24
|
-
fi
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
die() {
|
|
28
|
-
local code="$1"; shift
|
|
29
|
-
local safe_msg
|
|
30
|
-
safe_msg=$(json_escape "$*")
|
|
31
|
-
printf '{"status":"error","code":%d,"message":"%s"}\n' "$code" "$safe_msg" >&1
|
|
32
|
-
exit "$code"
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
# ── argument parsing ────────────────────────────────────────────────────────────
|
|
36
|
-
|
|
37
|
-
PLAN_PATH=''
|
|
38
|
-
BRANCH_NAME=''
|
|
39
|
-
HANDOFF=''
|
|
40
|
-
TEST=''
|
|
41
|
-
KILL=''
|
|
42
|
-
KILL_AND_RELAUNCH=''
|
|
43
|
-
|
|
44
|
-
while [[ $# -gt 0 ]]; do
|
|
45
|
-
case "$1" in
|
|
46
|
-
--handoff)
|
|
47
|
-
HANDOFF="$2"; shift 2 ;;
|
|
48
|
-
--handoff=*)
|
|
49
|
-
HANDOFF="${1#--handoff=}"; shift ;;
|
|
50
|
-
--test)
|
|
51
|
-
TEST=1; shift ;;
|
|
52
|
-
--kill)
|
|
53
|
-
KILL=1; shift ;;
|
|
54
|
-
--kill-and-relaunch)
|
|
55
|
-
KILL_AND_RELAUNCH=1; shift ;;
|
|
56
|
-
*)
|
|
57
|
-
if [[ -z "$PLAN_PATH" ]]; then
|
|
58
|
-
PLAN_PATH="$1"
|
|
59
|
-
elif [[ -z "$BRANCH_NAME" ]]; then
|
|
60
|
-
BRANCH_NAME="$1"
|
|
61
|
-
else
|
|
62
|
-
die 1 "Unexpected argument: $1"
|
|
63
|
-
fi
|
|
64
|
-
shift ;;
|
|
65
|
-
esac
|
|
66
|
-
done
|
|
67
|
-
|
|
68
|
-
if [[ -n "$KILL" && -n "$KILL_AND_RELAUNCH" ]]; then
|
|
69
|
-
die 1 "--kill and --kill-and-relaunch cannot be used together"
|
|
70
|
-
fi
|
|
71
|
-
|
|
72
|
-
if [[ -n "$KILL" ]]; then
|
|
73
|
-
# kill needs ONLY the branch name. Accept either "<branch-name> --kill" (a
|
|
74
|
-
# single positional) or "<plan-path> <branch-name> --kill" (the plan path is
|
|
75
|
-
# ignored — kill sends none). When only one positional was given it lands in
|
|
76
|
-
# PLAN_PATH, so re-home it as the branch name.
|
|
77
|
-
if [[ -z "$BRANCH_NAME" ]]; then
|
|
78
|
-
BRANCH_NAME="$PLAN_PATH"
|
|
79
|
-
PLAN_PATH=''
|
|
80
|
-
fi
|
|
81
|
-
if [[ -z "$BRANCH_NAME" ]]; then
|
|
82
|
-
die 1 "Usage: bash .github/scripts/hula-launch-run.sh <branch-name> --kill"
|
|
83
|
-
fi
|
|
84
|
-
else
|
|
85
|
-
if [[ -z "$PLAN_PATH" || -z "$BRANCH_NAME" ]]; then
|
|
86
|
-
die 1 "Usage: bash .github/scripts/hula-launch-run.sh <plan-path> <branch-name> [--handoff <username>] [--test] [--kill-and-relaunch]"
|
|
87
|
-
fi
|
|
88
|
-
fi
|
|
89
|
-
|
|
90
|
-
# Reject paths with directory traversal sequences (only when a plan is in play)
|
|
91
|
-
if [[ -n "$PLAN_PATH" ]] && printf '%s' "$PLAN_PATH" | grep -q '\.\.'; then
|
|
92
|
-
die 1 "Invalid plan path (path traversal not allowed): ${PLAN_PATH}"
|
|
93
|
-
fi
|
|
94
|
-
|
|
95
|
-
# ── Step 4: Run hula launch ─────────────────────────────────────────────────
|
|
96
|
-
|
|
97
|
-
if [[ -n "$KILL" ]]; then
|
|
98
|
-
printf '🛑 Stopping in-flight task for branch: %s\n' "$BRANCH_NAME" >&2
|
|
99
|
-
else
|
|
100
|
-
printf '🚀 Launching issue from plan: %s\n' "$PLAN_PATH" >&2
|
|
101
|
-
fi
|
|
102
|
-
|
|
103
|
-
# Build the launch invocation as an args array so the flags compose without
|
|
104
|
-
# combinatorial if/else branching. Branch name is always first; the plan path is
|
|
105
|
-
# a positional appended only when present (a bare --kill sends no plan path).
|
|
106
|
-
LAUNCH_ARGS=("$BRANCH_NAME")
|
|
107
|
-
if [[ -n "$PLAN_PATH" && -z "$KILL" ]]; then
|
|
108
|
-
LAUNCH_ARGS+=("$PLAN_PATH")
|
|
109
|
-
fi
|
|
110
|
-
if [[ -n "$HANDOFF" ]]; then
|
|
111
|
-
LAUNCH_ARGS+=(--handoff "$HANDOFF")
|
|
112
|
-
fi
|
|
113
|
-
if [[ -n "$TEST" ]]; then
|
|
114
|
-
LAUNCH_ARGS+=(--test)
|
|
115
|
-
fi
|
|
116
|
-
if [[ -n "$KILL" ]]; then
|
|
117
|
-
LAUNCH_ARGS+=(--kill)
|
|
118
|
-
fi
|
|
119
|
-
if [[ -n "$KILL_AND_RELAUNCH" ]]; then
|
|
120
|
-
LAUNCH_ARGS+=(--kill-and-relaunch)
|
|
121
|
-
fi
|
|
122
|
-
LAUNCH_OUTPUT=$(hula launch "${LAUNCH_ARGS[@]}" 2>&1) \
|
|
123
|
-
|| die 2 "Launch failed: $LAUNCH_OUTPUT"
|
|
124
|
-
|
|
125
|
-
# Extract issue number from CLI output (e.g. "Created issue #42" or "issue #42")
|
|
126
|
-
ISSUE_NUMBER=$(printf '%s' "$LAUNCH_OUTPUT" | grep -oE '#[0-9]+' | head -1 | tr -d '#') || true
|
|
127
|
-
|
|
128
|
-
# ── Output result ─────────────────────────────────────────────────────────────
|
|
129
|
-
|
|
130
|
-
SAFE_PLAN=$(json_escape "$PLAN_PATH")
|
|
131
|
-
SAFE_BRANCH=$(json_escape "$BRANCH_NAME")
|
|
132
|
-
SAFE_HANDOFF=$(json_escape "$HANDOFF")
|
|
133
|
-
SAFE_OUTPUT=$(json_escape "$LAUNCH_OUTPUT")
|
|
134
|
-
|
|
135
|
-
printf '{"status":"success","issueNumber":"%s","branchName":"%s","planPath":"%s","handoff":"%s","cliOutput":"%s"}\n' \
|
|
136
|
-
"${ISSUE_NUMBER:-}" \
|
|
137
|
-
"$SAFE_BRANCH" \
|
|
138
|
-
"$SAFE_PLAN" \
|
|
139
|
-
"$SAFE_HANDOFF" \
|
|
140
|
-
"$SAFE_OUTPUT"
|
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# hula-merge-local.sh — Path A of the hula.merge workflow (local session merge)
|
|
3
|
-
# Usage: bash .github/scripts/hula-merge-local.sh <issue-number> <worktree-path> <commit-message> [--skip-tests] [--dry-run]
|
|
4
|
-
#
|
|
5
|
-
# Performs: commit + push from worktree → hula merge → remove worktree → delete session file
|
|
6
|
-
# Outputs structured JSON to stdout; all other output goes to stderr.
|
|
7
|
-
# Exit codes: 0=success, 1=user error, 2=tool error
|
|
8
|
-
|
|
9
|
-
set -euo pipefail
|
|
10
|
-
|
|
11
|
-
# ── helpers ────────────────────────────────────────────────────────────────────
|
|
12
|
-
|
|
13
|
-
json_escape() {
|
|
14
|
-
printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g; s/$/\\n/g' | tr -d '\n' | sed 's/\\n$//'
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
die() {
|
|
18
|
-
local code="$1"; shift
|
|
19
|
-
local safe_msg
|
|
20
|
-
safe_msg=$(json_escape "$*")
|
|
21
|
-
printf '{"status":"error","code":%d,"message":"%s"}\n' "$code" "$safe_msg" >&1
|
|
22
|
-
exit "$code"
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
# ── argument parsing ────────────────────────────────────────────────────────────
|
|
26
|
-
|
|
27
|
-
ISSUE_NUMBER=''
|
|
28
|
-
WORKTREE_PATH=''
|
|
29
|
-
COMMIT_MSG=''
|
|
30
|
-
SKIP_TESTS=false
|
|
31
|
-
DRY_RUN=false
|
|
32
|
-
|
|
33
|
-
while [[ $# -gt 0 ]]; do
|
|
34
|
-
case "$1" in
|
|
35
|
-
--skip-tests) SKIP_TESTS=true; shift ;;
|
|
36
|
-
--dry-run) DRY_RUN=true; shift ;;
|
|
37
|
-
*)
|
|
38
|
-
if [[ -z "$ISSUE_NUMBER" ]]; then
|
|
39
|
-
ISSUE_NUMBER="$1"
|
|
40
|
-
elif [[ -z "$WORKTREE_PATH" ]]; then
|
|
41
|
-
WORKTREE_PATH="$1"
|
|
42
|
-
elif [[ -z "$COMMIT_MSG" ]]; then
|
|
43
|
-
COMMIT_MSG="$1"
|
|
44
|
-
else
|
|
45
|
-
die 1 "Unexpected argument: $1"
|
|
46
|
-
fi
|
|
47
|
-
shift ;;
|
|
48
|
-
esac
|
|
49
|
-
done
|
|
50
|
-
|
|
51
|
-
if [[ -z "$ISSUE_NUMBER" || -z "$WORKTREE_PATH" || -z "$COMMIT_MSG" ]]; then
|
|
52
|
-
die 1 "Usage: bash .github/scripts/hula-merge-local.sh <issue-number> <worktree-path> <commit-message> [--skip-tests] [--dry-run]"
|
|
53
|
-
fi
|
|
54
|
-
|
|
55
|
-
if [[ ! -d "$WORKTREE_PATH" ]]; then
|
|
56
|
-
die 1 "Worktree not found at ${WORKTREE_PATH}."
|
|
57
|
-
fi
|
|
58
|
-
|
|
59
|
-
# Compute absolute session file path before cd (so it resolves correctly later)
|
|
60
|
-
REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null) || REPO_ROOT="$(pwd)"
|
|
61
|
-
SESSION_FILE="${REPO_ROOT}/.hublaunch/.fix-sessions/issue-${ISSUE_NUMBER}.json"
|
|
62
|
-
|
|
63
|
-
# ── Step A3: Commit and push from worktree ────────────────────────────────────
|
|
64
|
-
|
|
65
|
-
cd "$WORKTREE_PATH"
|
|
66
|
-
|
|
67
|
-
PORCELAIN=$(git status --porcelain 2>/dev/null)
|
|
68
|
-
COMMITTED=false
|
|
69
|
-
FILES_CHANGED=0
|
|
70
|
-
|
|
71
|
-
if [[ -n "$PORCELAIN" ]]; then
|
|
72
|
-
FILES_CHANGED=$(printf '%s' "$PORCELAIN" | grep -c '' || true)
|
|
73
|
-
|
|
74
|
-
if [[ "$DRY_RUN" == false ]]; then
|
|
75
|
-
git add -A >&2 2>&1 \
|
|
76
|
-
|| die 2 "git add failed in worktree ${WORKTREE_PATH}"
|
|
77
|
-
|
|
78
|
-
git commit -m "$COMMIT_MSG" >&2 2>&1 \
|
|
79
|
-
|| die 2 "Failed to commit changes. Ensure git user.name and user.email are configured."
|
|
80
|
-
|
|
81
|
-
git push >&2 2>&1 \
|
|
82
|
-
|| die 2 "Push failed. Try: cd ${WORKTREE_PATH} && git push"
|
|
83
|
-
|
|
84
|
-
COMMITTED=true
|
|
85
|
-
else
|
|
86
|
-
printf '🔍 Dry run: would commit %d file(s) with message: %s\n' "$FILES_CHANGED" "$COMMIT_MSG" >&2
|
|
87
|
-
COMMITTED=false
|
|
88
|
-
fi
|
|
89
|
-
else
|
|
90
|
-
printf 'ℹ️ No changes to commit, proceeding to merge...\n' >&2
|
|
91
|
-
fi
|
|
92
|
-
|
|
93
|
-
# ── Step A4: Merge the PR ─────────────────────────────────────────────────────
|
|
94
|
-
|
|
95
|
-
# Return to the main repo root (parent of worktree if possible)
|
|
96
|
-
cd - > /dev/null 2>&1 || true
|
|
97
|
-
|
|
98
|
-
if [[ "$DRY_RUN" == false ]]; then
|
|
99
|
-
printf '🔀 Merging PR for issue #%s...\n' "$ISSUE_NUMBER" >&2
|
|
100
|
-
hula merge "$ISSUE_NUMBER" >&2 2>&1 \
|
|
101
|
-
|| die 2 "Failed to merge PR for issue #${ISSUE_NUMBER}. Session preserved — run /hula-merge again after resolving."
|
|
102
|
-
fi
|
|
103
|
-
|
|
104
|
-
# ── Step A5: Remove fix worktree ──────────────────────────────────────────────
|
|
105
|
-
|
|
106
|
-
if [[ "$DRY_RUN" == false ]]; then
|
|
107
|
-
printf '🧹 Removing fix worktree: %s\n' "$WORKTREE_PATH" >&2
|
|
108
|
-
git worktree remove "$WORKTREE_PATH" --force >&2 2>&1 \
|
|
109
|
-
|| printf '⚠️ Could not remove worktree at %s (may already be gone)\n' "$WORKTREE_PATH" >&2
|
|
110
|
-
fi
|
|
111
|
-
|
|
112
|
-
# ── Step A6: Clean up session file ───────────────────────────────────────────
|
|
113
|
-
|
|
114
|
-
SESSION_CLEANED=false
|
|
115
|
-
|
|
116
|
-
if [[ -f "$SESSION_FILE" ]]; then
|
|
117
|
-
if [[ "$DRY_RUN" == false ]]; then
|
|
118
|
-
rm -f "$SESSION_FILE"
|
|
119
|
-
SESSION_CLEANED=true
|
|
120
|
-
printf '🧹 Cleaned up session file\n' >&2
|
|
121
|
-
else
|
|
122
|
-
printf '🔍 Dry run: would delete session file %s\n' "$SESSION_FILE" >&2
|
|
123
|
-
fi
|
|
124
|
-
fi
|
|
125
|
-
|
|
126
|
-
# ── Output result ─────────────────────────────────────────────────────────────
|
|
127
|
-
|
|
128
|
-
SAFE_MSG=$(json_escape "$COMMIT_MSG")
|
|
129
|
-
SAFE_PATH=$(json_escape "$WORKTREE_PATH")
|
|
130
|
-
|
|
131
|
-
printf '{"status":"success","issueNumber":%s,"committed":%s,"filesChanged":%d,"worktreeRemoved":%s,"sessionCleaned":%s,"commitMessage":"%s","worktreePath":"%s"}\n' \
|
|
132
|
-
"$ISSUE_NUMBER" \
|
|
133
|
-
"$([ "$COMMITTED" == true ] && echo 'true' || echo 'false')" \
|
|
134
|
-
"$FILES_CHANGED" \
|
|
135
|
-
"$([ "$DRY_RUN" == false ] && echo 'true' || echo 'false')" \
|
|
136
|
-
"$([ "$SESSION_CLEANED" == true ] && echo 'true' || echo 'false')" \
|
|
137
|
-
"$SAFE_MSG" \
|
|
138
|
-
"$SAFE_PATH"
|