hub-launch 1.3.0 → 1.4.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 +16 -0
- package/README.md +20 -0
- package/dist/commands/execute.d.ts.map +1 -1
- package/dist/commands/execute.js +109 -0
- package/dist/commands/execute.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +20 -1
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/upload.d.ts.map +1 -1
- package/dist/commands/upload.js +16 -75
- package/dist/commands/upload.js.map +1 -1
- package/dist/services/git/FilePublishService.d.ts +68 -0
- package/dist/services/git/FilePublishService.d.ts.map +1 -0
- package/dist/services/git/FilePublishService.js +141 -0
- package/dist/services/git/FilePublishService.js.map +1 -0
- package/dist/templates/scripts/hula-execute-manage.sh +159 -0
- package/dist/templates/skill-creation-instructions.md +142 -0
- package/dist/templates/skills/hula-execute/SKILL.md +191 -61
- package/package.json +1 -1
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { Config } from '../../types/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Options shared by publish/delete operations.
|
|
4
|
+
*/
|
|
5
|
+
export interface FilePublishOptions {
|
|
6
|
+
/** Commit message to use for the change. */
|
|
7
|
+
commitMessage?: string;
|
|
8
|
+
/** Target branch to publish to. Defaults to `config.uploadBranch ?? 'main'`. */
|
|
9
|
+
branch?: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* FilePublishService — commit a single repo-relative file to a remote branch
|
|
13
|
+
* (default `origin/main`) using a temporary detached worktree, without ever
|
|
14
|
+
* touching the user's current branch or working tree.
|
|
15
|
+
*
|
|
16
|
+
* This encapsulates the worktree publish/delete dance that was previously
|
|
17
|
+
* inlined in `executeUpload`. Both plan upload and `/hula-execute` skill
|
|
18
|
+
* publishing share this one tested implementation.
|
|
19
|
+
*
|
|
20
|
+
* The flow for each operation is:
|
|
21
|
+
* 1. validate the target branch (injection guard)
|
|
22
|
+
* 2. resolve an absolute worktree base path
|
|
23
|
+
* 3. create a detached worktree off `origin/<branch>`
|
|
24
|
+
* 4. write (publish) or remove (delete) the file at `<worktree>/<relativePath>`
|
|
25
|
+
* 5. stage + commit (`git add .` stages deletions too) when there are changes
|
|
26
|
+
* 6. push `HEAD:<branch>`
|
|
27
|
+
* 7. always remove the worktree in `finally`
|
|
28
|
+
*/
|
|
29
|
+
export declare class FilePublishService {
|
|
30
|
+
private readonly config;
|
|
31
|
+
constructor(config: Config);
|
|
32
|
+
/**
|
|
33
|
+
* Default branch this service publishes to.
|
|
34
|
+
*/
|
|
35
|
+
private defaultBranch;
|
|
36
|
+
/**
|
|
37
|
+
* Validate a branch value to prevent command injection in the git push
|
|
38
|
+
* refspec. Mirrors the guard in `executeUpload`.
|
|
39
|
+
*/
|
|
40
|
+
private validateBranch;
|
|
41
|
+
/**
|
|
42
|
+
* Resolve the absolute worktree base path from config, identical to
|
|
43
|
+
* `executeUpload`'s behavior.
|
|
44
|
+
*/
|
|
45
|
+
private resolveWorktreeBasePath;
|
|
46
|
+
/**
|
|
47
|
+
* Run `operation` inside a fresh detached worktree off `origin/<branch>`,
|
|
48
|
+
* commit (with `commitMessage`) if it produced changes, push to the branch,
|
|
49
|
+
* and always remove the worktree afterward.
|
|
50
|
+
*/
|
|
51
|
+
private withWorktree;
|
|
52
|
+
/**
|
|
53
|
+
* Publish (create or update) a file on the target branch.
|
|
54
|
+
*
|
|
55
|
+
* @param relativePath - Repo-relative path of the file to write.
|
|
56
|
+
* @param content - The full content to write.
|
|
57
|
+
* @param opts - Optional commit message and branch override.
|
|
58
|
+
*/
|
|
59
|
+
publishFile(relativePath: string, content: string, opts?: FilePublishOptions): Promise<void>;
|
|
60
|
+
/**
|
|
61
|
+
* Delete a file from the target branch.
|
|
62
|
+
*
|
|
63
|
+
* @param relativePath - Repo-relative path of the file to remove.
|
|
64
|
+
* @param opts - Optional commit message and branch override.
|
|
65
|
+
*/
|
|
66
|
+
deleteFile(relativePath: string, opts?: FilePublishOptions): Promise<void>;
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=FilePublishService.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FilePublishService.d.ts","sourceRoot":"","sources":["../../../src/services/git/FilePublishService.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAKnD;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,4CAA4C;IAC5C,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,gFAAgF;IAChF,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;gBAEpB,MAAM,EAAE,MAAM;IAI1B;;OAEG;IACH,OAAO,CAAC,aAAa;IAIrB;;;OAGG;IACH,OAAO,CAAC,cAAc;IAiBtB;;;OAGG;YACW,uBAAuB;IAQrC;;;;OAIG;YACW,YAAY;IAuD1B;;;;;;OAMG;IACG,WAAW,CACf,YAAY,EAAE,MAAM,EACpB,OAAO,EAAE,MAAM,EACf,IAAI,CAAC,EAAE,kBAAkB,GACxB,OAAO,CAAC,IAAI,CAAC;IAWhB;;;;;OAKG;IACG,UAAU,CACd,YAAY,EAAE,MAAM,EACpB,IAAI,CAAC,EAAE,kBAAkB,GACxB,OAAO,CAAC,IAAI,CAAC;CAWjB"}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { dirname, isAbsolute, join, resolve } from 'path';
|
|
2
|
+
import { mkdirSync, rmSync, writeFileSync } from 'fs';
|
|
3
|
+
import { logger } from '../../utils/logger.js';
|
|
4
|
+
import { WorktreeService } from './WorktreeService.js';
|
|
5
|
+
import { getRepoInfo } from '../../utils/github-cli.js';
|
|
6
|
+
import { getRepoRoot } from '../../utils/shell.js';
|
|
7
|
+
/**
|
|
8
|
+
* FilePublishService — commit a single repo-relative file to a remote branch
|
|
9
|
+
* (default `origin/main`) using a temporary detached worktree, without ever
|
|
10
|
+
* touching the user's current branch or working tree.
|
|
11
|
+
*
|
|
12
|
+
* This encapsulates the worktree publish/delete dance that was previously
|
|
13
|
+
* inlined in `executeUpload`. Both plan upload and `/hula-execute` skill
|
|
14
|
+
* publishing share this one tested implementation.
|
|
15
|
+
*
|
|
16
|
+
* The flow for each operation is:
|
|
17
|
+
* 1. validate the target branch (injection guard)
|
|
18
|
+
* 2. resolve an absolute worktree base path
|
|
19
|
+
* 3. create a detached worktree off `origin/<branch>`
|
|
20
|
+
* 4. write (publish) or remove (delete) the file at `<worktree>/<relativePath>`
|
|
21
|
+
* 5. stage + commit (`git add .` stages deletions too) when there are changes
|
|
22
|
+
* 6. push `HEAD:<branch>`
|
|
23
|
+
* 7. always remove the worktree in `finally`
|
|
24
|
+
*/
|
|
25
|
+
export class FilePublishService {
|
|
26
|
+
config;
|
|
27
|
+
constructor(config) {
|
|
28
|
+
this.config = config;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Default branch this service publishes to.
|
|
32
|
+
*/
|
|
33
|
+
defaultBranch() {
|
|
34
|
+
return this.config.uploadBranch ?? 'main';
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Validate a branch value to prevent command injection in the git push
|
|
38
|
+
* refspec. Mirrors the guard in `executeUpload`.
|
|
39
|
+
*/
|
|
40
|
+
validateBranch(branch) {
|
|
41
|
+
if (branch.includes(';') ||
|
|
42
|
+
branch.includes('|') ||
|
|
43
|
+
branch.includes('&') ||
|
|
44
|
+
branch.includes('$') ||
|
|
45
|
+
branch.includes('`') ||
|
|
46
|
+
branch.includes('\\') ||
|
|
47
|
+
branch.includes('\n') ||
|
|
48
|
+
branch.includes('\r') ||
|
|
49
|
+
branch.includes('\0') ||
|
|
50
|
+
branch.trim() === '') {
|
|
51
|
+
throw new Error(`Invalid branch value: "${branch}"`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Resolve the absolute worktree base path from config, identical to
|
|
56
|
+
* `executeUpload`'s behavior.
|
|
57
|
+
*/
|
|
58
|
+
async resolveWorktreeBasePath() {
|
|
59
|
+
const rawBasePath = this.config.worktreeBasePath ?? '.hula-worktrees';
|
|
60
|
+
const repoRoot = await getRepoRoot();
|
|
61
|
+
return isAbsolute(rawBasePath)
|
|
62
|
+
? rawBasePath
|
|
63
|
+
: resolve(repoRoot, rawBasePath);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Run `operation` inside a fresh detached worktree off `origin/<branch>`,
|
|
67
|
+
* commit (with `commitMessage`) if it produced changes, push to the branch,
|
|
68
|
+
* and always remove the worktree afterward.
|
|
69
|
+
*/
|
|
70
|
+
async withWorktree(type, branch, commitMessage, operation) {
|
|
71
|
+
this.validateBranch(branch);
|
|
72
|
+
const worktreeBasePath = await this.resolveWorktreeBasePath();
|
|
73
|
+
const repoInfo = await getRepoInfo();
|
|
74
|
+
const worktreeService = new WorktreeService({
|
|
75
|
+
basePath: worktreeBasePath,
|
|
76
|
+
repoName: repoInfo.name,
|
|
77
|
+
});
|
|
78
|
+
// Timestamp-based identifier to avoid collisions between concurrent publishes.
|
|
79
|
+
const identifier = `${Date.now()}`;
|
|
80
|
+
const worktreePath = worktreeService.getWorktreeDir(type, identifier);
|
|
81
|
+
let worktreeCreated = false;
|
|
82
|
+
try {
|
|
83
|
+
await worktreeService.createWorktreeAt(worktreePath, `origin/${branch}`, true);
|
|
84
|
+
worktreeCreated = true;
|
|
85
|
+
operation(worktreePath);
|
|
86
|
+
const hasChanges = await worktreeService.hasChangesAt(worktreePath);
|
|
87
|
+
if (hasChanges) {
|
|
88
|
+
await worktreeService.commitChangesAt(worktreePath, commitMessage);
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
logger.info('No changes to commit, proceeding to push...');
|
|
92
|
+
}
|
|
93
|
+
// For a detached HEAD worktree we push HEAD directly to the target branch.
|
|
94
|
+
await worktreeService.pushAt(worktreePath, `HEAD:${branch}`);
|
|
95
|
+
}
|
|
96
|
+
finally {
|
|
97
|
+
if (worktreeCreated) {
|
|
98
|
+
try {
|
|
99
|
+
await worktreeService.removeWorktreeAt(worktreePath);
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
logger.warning(`Failed to remove worktree at ${worktreePath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
103
|
+
logger.info(`You can manually remove it with: git worktree remove "${worktreePath}" --force`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Publish (create or update) a file on the target branch.
|
|
110
|
+
*
|
|
111
|
+
* @param relativePath - Repo-relative path of the file to write.
|
|
112
|
+
* @param content - The full content to write.
|
|
113
|
+
* @param opts - Optional commit message and branch override.
|
|
114
|
+
*/
|
|
115
|
+
async publishFile(relativePath, content, opts) {
|
|
116
|
+
const branch = opts?.branch ?? this.defaultBranch();
|
|
117
|
+
const commitMessage = opts?.commitMessage ?? 'chore: publish file via hula';
|
|
118
|
+
await this.withWorktree('publish', branch, commitMessage, (worktreePath) => {
|
|
119
|
+
const filePath = join(worktreePath, relativePath);
|
|
120
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
121
|
+
writeFileSync(filePath, content, 'utf-8');
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Delete a file from the target branch.
|
|
126
|
+
*
|
|
127
|
+
* @param relativePath - Repo-relative path of the file to remove.
|
|
128
|
+
* @param opts - Optional commit message and branch override.
|
|
129
|
+
*/
|
|
130
|
+
async deleteFile(relativePath, opts) {
|
|
131
|
+
const branch = opts?.branch ?? this.defaultBranch();
|
|
132
|
+
const commitMessage = opts?.commitMessage ?? 'chore: remove file via hula';
|
|
133
|
+
await this.withWorktree('publish', branch, commitMessage, (worktreePath) => {
|
|
134
|
+
const filePath = join(worktreePath, relativePath);
|
|
135
|
+
// `commitChangesAt` runs `git add .`, which stages deletions, so a plain
|
|
136
|
+
// filesystem remove is sufficient here.
|
|
137
|
+
rmSync(filePath, { force: true });
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
//# sourceMappingURL=FilePublishService.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FilePublishService.js","sourceRoot":"","sources":["../../../src/services/git/FilePublishService.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# hula-execute-manage.sh — management surface for the /hula-execute skill
|
|
3
|
+
# Usage (exactly one action per call):
|
|
4
|
+
# bash .github/scripts/hula-execute-manage.sh --list
|
|
5
|
+
# bash .github/scripts/hula-execute-manage.sh --list-schedules
|
|
6
|
+
# bash .github/scripts/hula-execute-manage.sh --show <runId>
|
|
7
|
+
# bash .github/scripts/hula-execute-manage.sh --run-now <scheduleId>
|
|
8
|
+
# bash .github/scripts/hula-execute-manage.sh --cancel-schedule <scheduleId>
|
|
9
|
+
# bash .github/scripts/hula-execute-manage.sh --publish-skill <repo-relative-path>
|
|
10
|
+
# bash .github/scripts/hula-execute-manage.sh --delete-skill <repo-relative-path>
|
|
11
|
+
#
|
|
12
|
+
# Each invocation maps to the corresponding `hula execute …` call, captures its
|
|
13
|
+
# output, and emits a single structured JSON object to stdout; all other output
|
|
14
|
+
# goes to stderr.
|
|
15
|
+
#
|
|
16
|
+
# JSON contract (stdout):
|
|
17
|
+
# read verbs : {"status":"success","kind":"list|list-schedules|show","cliOutput":"…"}
|
|
18
|
+
# run-now : {"status":"success","kind":"run-now","runId":"…","cliOutput":"…"}
|
|
19
|
+
# cancel : {"status":"success","kind":"cancel-schedule","scheduleId":"…","cliOutput":"…"}
|
|
20
|
+
# publish-skill : {"status":"success","kind":"publish-skill","path":"…","cliOutput":"…"}
|
|
21
|
+
# delete-skill : {"status":"success","kind":"delete-skill","path":"…","cliOutput":"…"}
|
|
22
|
+
# failure : {"status":"error","code":<n>,"message":"…"}
|
|
23
|
+
#
|
|
24
|
+
# Exit codes: 0=success, 1=user error, 2=tool error
|
|
25
|
+
#
|
|
26
|
+
# Security: never prints secrets. Credentials are resolved inside the CLI from
|
|
27
|
+
# flags/config/env — they are NOT passed by this script. --publish-skill and
|
|
28
|
+
# --delete-skill reject path traversal and require a .hublaunch/skills/ path.
|
|
29
|
+
|
|
30
|
+
set -euo pipefail
|
|
31
|
+
|
|
32
|
+
# ── helpers ────────────────────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
json_escape() {
|
|
35
|
+
if command -v jq &>/dev/null; then
|
|
36
|
+
# jq -Rs . reads stdin as a raw string and outputs a JSON string (with surrounding quotes).
|
|
37
|
+
# Strip the surrounding quotes; the escaped content is safe to embed in JSON.
|
|
38
|
+
printf '%s' "$1" | jq -Rs . | sed 's/^"//;s/"$//'
|
|
39
|
+
else
|
|
40
|
+
# Fallback: escape backslashes and double quotes, collapse control chars to spaces
|
|
41
|
+
printf '%s' "$1" \
|
|
42
|
+
| sed 's/\\/\\\\/g; s/"/\\"/g' \
|
|
43
|
+
| tr '\n\r\t' ' '
|
|
44
|
+
fi
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
die() {
|
|
48
|
+
local code="$1"; shift
|
|
49
|
+
local safe_msg
|
|
50
|
+
safe_msg=$(json_escape "$*")
|
|
51
|
+
printf '{"status":"error","code":%d,"message":"%s"}\n' "$code" "$safe_msg" >&1
|
|
52
|
+
exit "$code"
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
# Validate a skill path: must be repo-relative, under .hublaunch/skills/, no traversal.
|
|
56
|
+
validate_skill_path() {
|
|
57
|
+
local p="$1"
|
|
58
|
+
case "$p" in
|
|
59
|
+
/*) die 1 "Skill path must be repo-relative, not absolute: ${p}" ;;
|
|
60
|
+
esac
|
|
61
|
+
if printf '%s' "$p" | grep -q '\.\.'; then
|
|
62
|
+
die 1 "Invalid skill path (path traversal not allowed): ${p}"
|
|
63
|
+
fi
|
|
64
|
+
case "$p" in
|
|
65
|
+
.hublaunch/skills/*) : ;;
|
|
66
|
+
*) die 1 "Skill path must be under .hublaunch/skills/ — got ${p}" ;;
|
|
67
|
+
esac
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
# ── argument parsing ────────────────────────────────────────────────────────────
|
|
71
|
+
# Exactly one action flag is allowed per call.
|
|
72
|
+
|
|
73
|
+
ACTION=''
|
|
74
|
+
ARG=''
|
|
75
|
+
|
|
76
|
+
set_action() {
|
|
77
|
+
if [[ -n "$ACTION" ]]; then
|
|
78
|
+
die 1 "Provide exactly one action (got --$ACTION and $1)."
|
|
79
|
+
fi
|
|
80
|
+
ACTION="$1"
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
while [[ $# -gt 0 ]]; do
|
|
84
|
+
case "$1" in
|
|
85
|
+
--list) set_action 'list'; shift ;;
|
|
86
|
+
--list-schedules) set_action 'list-schedules'; shift ;;
|
|
87
|
+
--show) set_action 'show'; ARG="$2"; shift 2 ;;
|
|
88
|
+
--show=*) set_action 'show'; ARG="${1#--show=}"; shift ;;
|
|
89
|
+
--run-now) set_action 'run-now'; ARG="$2"; shift 2 ;;
|
|
90
|
+
--run-now=*) set_action 'run-now'; ARG="${1#--run-now=}"; shift ;;
|
|
91
|
+
--cancel-schedule) set_action 'cancel-schedule'; ARG="$2"; shift 2 ;;
|
|
92
|
+
--cancel-schedule=*) set_action 'cancel-schedule'; ARG="${1#--cancel-schedule=}"; shift ;;
|
|
93
|
+
--publish-skill) set_action 'publish-skill'; ARG="$2"; shift 2 ;;
|
|
94
|
+
--publish-skill=*) set_action 'publish-skill'; ARG="${1#--publish-skill=}"; shift ;;
|
|
95
|
+
--delete-skill) set_action 'delete-skill'; ARG="$2"; shift 2 ;;
|
|
96
|
+
--delete-skill=*) set_action 'delete-skill'; ARG="${1#--delete-skill=}"; shift ;;
|
|
97
|
+
*) die 1 "Unexpected argument: $1" ;;
|
|
98
|
+
esac
|
|
99
|
+
done
|
|
100
|
+
|
|
101
|
+
if [[ -z "$ACTION" ]]; then
|
|
102
|
+
die 1 "Usage: bash .github/scripts/hula-execute-manage.sh (--list | --list-schedules | --show <id> | --run-now <id> | --cancel-schedule <id> | --publish-skill <path> | --delete-skill <path>)"
|
|
103
|
+
fi
|
|
104
|
+
|
|
105
|
+
# Actions that require an argument.
|
|
106
|
+
case "$ACTION" in
|
|
107
|
+
show|run-now|cancel-schedule|publish-skill|delete-skill)
|
|
108
|
+
[[ -n "$ARG" ]] || die 1 "--$ACTION requires an argument." ;;
|
|
109
|
+
esac
|
|
110
|
+
|
|
111
|
+
# Path validation for skill file actions.
|
|
112
|
+
case "$ACTION" in
|
|
113
|
+
publish-skill|delete-skill) validate_skill_path "$ARG" ;;
|
|
114
|
+
esac
|
|
115
|
+
|
|
116
|
+
# ── Build and run the hula execute invocation ────────────────────────────────
|
|
117
|
+
|
|
118
|
+
CLI_ARGS=()
|
|
119
|
+
case "$ACTION" in
|
|
120
|
+
list) CLI_ARGS=(--list) ;;
|
|
121
|
+
list-schedules) CLI_ARGS=(--list-schedules) ;;
|
|
122
|
+
show) CLI_ARGS=(--show "$ARG") ;;
|
|
123
|
+
run-now) CLI_ARGS=(--run-now "$ARG") ;;
|
|
124
|
+
cancel-schedule) CLI_ARGS=(--cancel-schedule "$ARG") ;;
|
|
125
|
+
publish-skill) CLI_ARGS=(--publish-skill "$ARG") ;;
|
|
126
|
+
delete-skill) CLI_ARGS=(--delete-skill "$ARG") ;;
|
|
127
|
+
esac
|
|
128
|
+
|
|
129
|
+
printf '⚙️ hula execute --%s...\n' "$ACTION" >&2
|
|
130
|
+
|
|
131
|
+
OUTPUT=$(hula execute "${CLI_ARGS[@]}" 2>&1) || die 2 "Management action failed: $OUTPUT"
|
|
132
|
+
|
|
133
|
+
SAFE_OUTPUT=$(json_escape "$OUTPUT")
|
|
134
|
+
|
|
135
|
+
# ── Emit the structured JSON result ──────────────────────────────────────────
|
|
136
|
+
|
|
137
|
+
case "$ACTION" in
|
|
138
|
+
list|list-schedules|show)
|
|
139
|
+
printf '{"status":"success","kind":"%s","cliOutput":"%s"}\n' \
|
|
140
|
+
"$ACTION" "$SAFE_OUTPUT"
|
|
141
|
+
;;
|
|
142
|
+
run-now)
|
|
143
|
+
# The CLI prints " ID: <id>" for the new run.
|
|
144
|
+
RUN_ID=$(printf '%s' "$OUTPUT" | grep -E '^\s*ID:' | head -1 | sed -E 's/^\s*ID:\s*//') || true
|
|
145
|
+
SAFE_ID=$(json_escape "$RUN_ID")
|
|
146
|
+
printf '{"status":"success","kind":"run-now","runId":"%s","cliOutput":"%s"}\n' \
|
|
147
|
+
"$SAFE_ID" "$SAFE_OUTPUT"
|
|
148
|
+
;;
|
|
149
|
+
cancel-schedule)
|
|
150
|
+
SAFE_ID=$(json_escape "$ARG")
|
|
151
|
+
printf '{"status":"success","kind":"cancel-schedule","scheduleId":"%s","cliOutput":"%s"}\n' \
|
|
152
|
+
"$SAFE_ID" "$SAFE_OUTPUT"
|
|
153
|
+
;;
|
|
154
|
+
publish-skill|delete-skill)
|
|
155
|
+
SAFE_PATH=$(json_escape "$ARG")
|
|
156
|
+
printf '{"status":"success","kind":"%s","path":"%s","cliOutput":"%s"}\n' \
|
|
157
|
+
"$ACTION" "$SAFE_PATH" "$SAFE_OUTPUT"
|
|
158
|
+
;;
|
|
159
|
+
esac
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# Execute Skill Creation Instructions
|
|
2
|
+
|
|
3
|
+
These instructions drive the **create-from-description** mode of the
|
|
4
|
+
`/hula-execute` skill: turning a plain-language description into a published
|
|
5
|
+
action file that `hula execute` can run or schedule.
|
|
6
|
+
|
|
7
|
+
The action file is plain instruction markdown that the hula-project server reads
|
|
8
|
+
from `origin/<uploadBranch>` (default `main`) at run time. It is **not** an Agent
|
|
9
|
+
Skill and is never registered as a slash command — it lives under
|
|
10
|
+
`.hublaunch/skills/`.
|
|
11
|
+
|
|
12
|
+
Follow these steps in order. Do **not** skip the question step, and do **not**
|
|
13
|
+
run `hula execute` before the file is successfully published.
|
|
14
|
+
|
|
15
|
+
## Step 1: Ask clarifying questions first (then STOP)
|
|
16
|
+
|
|
17
|
+
Mirror the `/hula-plan` question-first behavior. Before writing anything, ask the
|
|
18
|
+
user the questions you cannot confidently answer from their description:
|
|
19
|
+
|
|
20
|
+
1. **What should the action do?** The concrete task to perform on each run
|
|
21
|
+
(e.g. "remove unreachable code", "upgrade dependencies and fix breakages").
|
|
22
|
+
2. **Target entry point** (optional): the file, directory, or URL the action
|
|
23
|
+
focuses on (e.g. `src/`). Maps to `--entry-point`.
|
|
24
|
+
3. **Outcome type**: `pr` (open a pull request), `plan` (produce a plan), or
|
|
25
|
+
`feedback` (review/report only). Maps to `--outcome-type`. Default `pr`.
|
|
26
|
+
4. **One-off or scheduled?** If recurring, get the schedule phrase (e.g. "every
|
|
27
|
+
night", "every Monday 9am").
|
|
28
|
+
5. **Any constraints / scope limits** the run must respect (optional).
|
|
29
|
+
|
|
30
|
+
**STOP and wait for the answers.** Do not proceed to authoring until the user
|
|
31
|
+
responds. If anything is still ambiguous after the answers, ask a focused
|
|
32
|
+
follow-up round.
|
|
33
|
+
|
|
34
|
+
## Step 2: Confirm the action name
|
|
35
|
+
|
|
36
|
+
Derive a default slug from the description: lowercase, hyphen-separated, no
|
|
37
|
+
spaces or punctuation (e.g. "remove unreachable code in src" →
|
|
38
|
+
`remove-unreachable-code`). Propose it to the user and let them override. Confirm
|
|
39
|
+
the final name before writing the file.
|
|
40
|
+
|
|
41
|
+
## Step 3: Resolve the schedule (if recurring)
|
|
42
|
+
|
|
43
|
+
If the action is scheduled, translate the phrase to a 5-field cron expression
|
|
44
|
+
using the preset table in `SKILL.md`, then **echo the cron with a plain-English
|
|
45
|
+
readback and confirm it** before publishing/running. There is no server-side
|
|
46
|
+
cron validation, so the readback + confirmation is mandatory.
|
|
47
|
+
|
|
48
|
+
## Step 4: Compute the filename and path
|
|
49
|
+
|
|
50
|
+
- Filename: `YYYY-MM-DD-HH:MM-<name-slug>.md`
|
|
51
|
+
- Use today's date and the current time on a 24-hour clock.
|
|
52
|
+
- `<name-slug>` is the confirmed, lowercase-hyphenated action name.
|
|
53
|
+
- Repo-relative path: `.hublaunch/skills/<filename>`
|
|
54
|
+
- Create the `.hublaunch/skills/` directory if it does not exist.
|
|
55
|
+
|
|
56
|
+
## Step 5: Write the action file (free-form instruction markdown, NO frontmatter)
|
|
57
|
+
|
|
58
|
+
Use the `Write` tool to create the file at the computed path. The content MUST be
|
|
59
|
+
free-form instruction markdown with **no YAML frontmatter** (frontmatter would
|
|
60
|
+
make it look like an Agent Skill). Follow this template:
|
|
61
|
+
|
|
62
|
+
```markdown
|
|
63
|
+
# <Action Title>
|
|
64
|
+
|
|
65
|
+
## Goal
|
|
66
|
+
<1–3 sentences describing what this action should accomplish.>
|
|
67
|
+
|
|
68
|
+
## Steps
|
|
69
|
+
1. <step>
|
|
70
|
+
2. <step>
|
|
71
|
+
3. <step>
|
|
72
|
+
|
|
73
|
+
## Constraints
|
|
74
|
+
- <optional: what must not change / scope limits>
|
|
75
|
+
|
|
76
|
+
## Outcome
|
|
77
|
+
<What the run should produce, consistent with the chosen --outcome-type:
|
|
78
|
+
pr | plan | feedback.>
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Fill the template from the user's answers. Omit the `## Constraints` section if
|
|
82
|
+
the user gave no constraints.
|
|
83
|
+
|
|
84
|
+
## Step 6: Publish to origin/main BEFORE running (mandatory ordering)
|
|
85
|
+
|
|
86
|
+
The server clones the repository and reads the action file from the default
|
|
87
|
+
branch at run time (and re-reads it on every scheduled fire). The file MUST be on
|
|
88
|
+
the branch before the run, so publish it first:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
bash .github/scripts/hula-execute-manage.sh --publish-skill .hublaunch/skills/<filename>
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Parse the single JSON object it prints:
|
|
95
|
+
|
|
96
|
+
- If `status` is `"error"`: display `❌ <message>` and **STOP**. Do **not** run
|
|
97
|
+
`hula execute` — the file is not on the branch, so the run would fail.
|
|
98
|
+
- If `status` is `"success"`: continue to Step 7.
|
|
99
|
+
|
|
100
|
+
## Step 7: Run or schedule the action
|
|
101
|
+
|
|
102
|
+
Only after a successful publish, invoke the run wrapper with `--action-path`
|
|
103
|
+
pointing at the committed file:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
bash .github/scripts/hula-execute-run.sh --action-path .hublaunch/skills/<filename> [--entry-point <path>] [--outcome-type <type>] [--schedule "<cron>"]
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Pass only the flags you resolved. Quote the cron expression.
|
|
110
|
+
|
|
111
|
+
## Step 8: Report the result
|
|
112
|
+
|
|
113
|
+
Parse the run wrapper's JSON (same contract as the normal run flow) and report:
|
|
114
|
+
|
|
115
|
+
- The **created file path** (`.hublaunch/skills/<filename>`) and that it was
|
|
116
|
+
published to `origin/<uploadBranch>`.
|
|
117
|
+
- The **run or schedule result**:
|
|
118
|
+
- One-off run → the Run ID and PR link (if any), plus
|
|
119
|
+
`hula execute --show <runId>` to check status.
|
|
120
|
+
- Schedule → the Schedule ID and cron expression, plus a note that you can
|
|
121
|
+
manage it with `/hula-execute list`, `/hula-execute run now <id>`,
|
|
122
|
+
`/hula-execute cancel <id>`, or `/hula-execute update <id> …`.
|
|
123
|
+
|
|
124
|
+
Example success report:
|
|
125
|
+
|
|
126
|
+
```
|
|
127
|
+
✅ Action created and published
|
|
128
|
+
|
|
129
|
+
📄 **File**: .hublaunch/skills/2026-06-19-16:35-remove-unreachable-code.md (on origin/main)
|
|
130
|
+
🔖 **Schedule ID**: sch_abc123
|
|
131
|
+
⏰ **Cron**: 0 3 * * * (every day at 3:00 AM)
|
|
132
|
+
|
|
133
|
+
Manage it with: /hula-execute list · /hula-execute run now sch_abc123 · /hula-execute cancel sch_abc123
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Notes
|
|
137
|
+
|
|
138
|
+
- Never echo secrets. Credentials resolve inside the CLI.
|
|
139
|
+
- The action file is committed to `origin/<uploadBranch>` via a temporary
|
|
140
|
+
worktree — the user's current branch and working tree are never touched.
|
|
141
|
+
- If the publish succeeds but the run fails, the file remains on the branch; the
|
|
142
|
+
user can re-run it later with `/hula-execute .hublaunch/skills/<filename>`.
|