hub-launch 1.3.0 → 1.4.1
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 +22 -0
- package/README.md +39 -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 +32 -2
- 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/index.js +34 -3
- package/dist/index.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/dist/types/config.schema.d.ts +3 -0
- package/dist/types/config.schema.d.ts.map +1 -1
- package/dist/types/config.schema.js +4 -0
- package/dist/types/config.schema.js.map +1 -1
- package/dist/utils/version.d.ts +22 -0
- package/dist/utils/version.d.ts.map +1 -0
- package/dist/utils/version.js +35 -0
- package/dist/utils/version.js.map +1 -0
- package/package.json +1 -1
package/dist/commands/upload.js
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
import { basename
|
|
2
|
-
import { readFileSync
|
|
1
|
+
import { basename } from 'path';
|
|
2
|
+
import { readFileSync } from 'fs';
|
|
3
3
|
import { PlanService } from '../services/plan/PlanService.js';
|
|
4
4
|
import { logger } from '../utils/logger.js';
|
|
5
|
-
import { WorktreeService } from '../services/git/WorktreeService.js';
|
|
6
5
|
import { GitService } from '../services/git/GitService.js';
|
|
7
|
-
import {
|
|
8
|
-
import { getRepoRoot } from '../utils/shell.js';
|
|
6
|
+
import { FilePublishService } from '../services/git/FilePublishService.js';
|
|
9
7
|
/**
|
|
10
8
|
* Upload command - Upload a plan to the target branch using a temporary worktree.
|
|
11
9
|
*
|
|
@@ -40,23 +38,6 @@ export function uploadCommand(program, config) {
|
|
|
40
38
|
export async function executeUpload(config, planPath) {
|
|
41
39
|
const planService = new PlanService(config.planPath);
|
|
42
40
|
const targetBranch = config.uploadBranch ?? 'main';
|
|
43
|
-
const rawBasePath = config.worktreeBasePath ?? '.hula-worktrees';
|
|
44
|
-
const repoRoot = await getRepoRoot();
|
|
45
|
-
const worktreeBasePath = isAbsolute(rawBasePath) ? rawBasePath : resolve(repoRoot, rawBasePath);
|
|
46
|
-
// Validate targetBranch to prevent command injection in git push refspec
|
|
47
|
-
if (targetBranch.includes(';') ||
|
|
48
|
-
targetBranch.includes('|') ||
|
|
49
|
-
targetBranch.includes('&') ||
|
|
50
|
-
targetBranch.includes('$') ||
|
|
51
|
-
targetBranch.includes('`') ||
|
|
52
|
-
targetBranch.includes('\\') ||
|
|
53
|
-
targetBranch.includes('\n') ||
|
|
54
|
-
targetBranch.includes('\r') ||
|
|
55
|
-
targetBranch.includes('\0') ||
|
|
56
|
-
targetBranch.trim() === '') {
|
|
57
|
-
logger.error(`Invalid uploadBranch value in config: "${targetBranch}"`);
|
|
58
|
-
process.exit(1);
|
|
59
|
-
}
|
|
60
41
|
// Step 1: Determine which plan to upload
|
|
61
42
|
let targetPlan = null;
|
|
62
43
|
if (planPath) {
|
|
@@ -99,61 +80,21 @@ export async function executeUpload(config, planPath) {
|
|
|
99
80
|
}
|
|
100
81
|
}
|
|
101
82
|
logger.debug('Plan file content read into memory');
|
|
102
|
-
// Step 3:
|
|
103
|
-
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
repoName: repoInfo.name,
|
|
107
|
-
});
|
|
108
|
-
// Use a timestamp-based identifier to avoid collisions between concurrent uploads
|
|
109
|
-
const identifier = `${Date.now()}`;
|
|
110
|
-
const worktreePath = worktreeService.getWorktreeDir('upload', identifier);
|
|
111
|
-
logger.info('Creating temporary upload worktree...');
|
|
112
|
-
let worktreeCreated = false;
|
|
83
|
+
// Step 3: Publish the plan file to the target branch via a temporary
|
|
84
|
+
// detached worktree (shared implementation with the /hula-execute skill).
|
|
85
|
+
const publisher = new FilePublishService(config);
|
|
86
|
+
const spinner = logger.spinner(`Uploading plan to origin/${targetBranch}...`);
|
|
113
87
|
try {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
mkdirSync(dirname(worktreePlanPath), { recursive: true });
|
|
121
|
-
writeFileSync(worktreePlanPath, planContent, 'utf-8');
|
|
122
|
-
// Step 6: Stage and commit in the worktree
|
|
123
|
-
logger.info('Committing plan file in upload worktree...');
|
|
124
|
-
const hasChanges = await worktreeService.hasChangesAt(worktreePath);
|
|
125
|
-
if (hasChanges) {
|
|
126
|
-
await worktreeService.commitChangesAt(worktreePath, 'chore: upload plan via hula');
|
|
127
|
-
}
|
|
128
|
-
else {
|
|
129
|
-
logger.info('Plan already up-to-date, proceeding to push...');
|
|
130
|
-
}
|
|
131
|
-
// Step 7: Push from worktree
|
|
132
|
-
const spinner = logger.spinner(`Pushing to origin/${targetBranch}...`);
|
|
133
|
-
try {
|
|
134
|
-
// For a detached HEAD worktree we push HEAD directly to the target branch
|
|
135
|
-
await worktreeService.pushAt(worktreePath, `HEAD:${targetBranch}`);
|
|
136
|
-
spinner.stop();
|
|
137
|
-
logger.success(`Plan uploaded to origin/${targetBranch}`);
|
|
138
|
-
}
|
|
139
|
-
catch (error) {
|
|
140
|
-
spinner.stop();
|
|
141
|
-
throw error;
|
|
142
|
-
}
|
|
88
|
+
await publisher.publishFile(relativePath, planContent, {
|
|
89
|
+
commitMessage: 'chore: upload plan via hula',
|
|
90
|
+
branch: targetBranch,
|
|
91
|
+
});
|
|
92
|
+
spinner.stop();
|
|
93
|
+
logger.success(`Plan uploaded to origin/${targetBranch}`);
|
|
143
94
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
logger.info('Cleaning up upload worktree...');
|
|
148
|
-
try {
|
|
149
|
-
await worktreeService.removeWorktreeAt(worktreePath);
|
|
150
|
-
logger.debug('Upload worktree removed');
|
|
151
|
-
}
|
|
152
|
-
catch (error) {
|
|
153
|
-
logger.warning(`Failed to remove upload worktree at ${worktreePath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
154
|
-
logger.info(`You can manually remove it with: git worktree remove "${worktreePath}" --force`);
|
|
155
|
-
}
|
|
156
|
-
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
spinner.stop();
|
|
97
|
+
throw error;
|
|
157
98
|
}
|
|
158
99
|
// Plan file remains untouched in the main working tree
|
|
159
100
|
// Note: the file will be cleaned up later by launch/create after success
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"upload.js","sourceRoot":"","sources":["../../src/commands/upload.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,
|
|
1
|
+
{"version":3,"file":"upload.js","sourceRoot":"","sources":["../../src/commands/upload.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -34,14 +34,17 @@ console.error = (...args) => {
|
|
|
34
34
|
}
|
|
35
35
|
return originalConsoleError.apply(console, args);
|
|
36
36
|
};
|
|
37
|
-
import {
|
|
37
|
+
import { existsSync } from 'fs';
|
|
38
|
+
import { join } from 'path';
|
|
38
39
|
import { Command } from 'commander';
|
|
39
40
|
import { logger } from './utils/logger.js';
|
|
40
41
|
import { validateGhCli } from './utils/github-cli.js';
|
|
41
42
|
import { loadConfig } from './config/index.js';
|
|
42
43
|
import { githubAppService } from './services/github/GithubAppService.js';
|
|
43
|
-
|
|
44
|
-
|
|
44
|
+
import { getCliVersion, isReinitNeeded } from './utils/version.js';
|
|
45
|
+
// Single source of truth for the running CLI version (used for --version and
|
|
46
|
+
// the runtime staleness check). Falls back to '0.0.0' only for program.version.
|
|
47
|
+
const version = getCliVersion() ?? '0.0.0';
|
|
45
48
|
// Import commands
|
|
46
49
|
import { initCommand } from './commands/init.js';
|
|
47
50
|
import { mergeCommand } from './commands/merge.js';
|
|
@@ -93,6 +96,34 @@ async function main() {
|
|
|
93
96
|
const config = await loadConfig(projectRoot);
|
|
94
97
|
// Stop spinner once loaded
|
|
95
98
|
spinner?.stop();
|
|
99
|
+
// Non-blocking reminder: if the project was initialized with a different
|
|
100
|
+
// CLI version, prompt the user to re-run `hula init` to refresh the bundled
|
|
101
|
+
// skills/templates/config. Wrapped in try/catch so it can never break the
|
|
102
|
+
// command. Excluded for `init` (the command that fixes it) and help/version.
|
|
103
|
+
if (!isHelpOrVersion && args[0] !== 'init') {
|
|
104
|
+
try {
|
|
105
|
+
const cliVersion = getCliVersion();
|
|
106
|
+
const configFileExists = [
|
|
107
|
+
'hublaunch.config.ts',
|
|
108
|
+
'hublaunch.config.mjs',
|
|
109
|
+
'hublaunch.config.js',
|
|
110
|
+
].some((f) => existsSync(join(projectRoot, '.hublaunch', f)));
|
|
111
|
+
if (isReinitNeeded({
|
|
112
|
+
configFileExists,
|
|
113
|
+
configVersion: config.version,
|
|
114
|
+
cliVersion,
|
|
115
|
+
})) {
|
|
116
|
+
const recorded = config.version
|
|
117
|
+
? `v${config.version}`
|
|
118
|
+
: 'an older version';
|
|
119
|
+
logger.warning(`hub-launch was updated to v${cliVersion} (this project was initialized with ${recorded}).`);
|
|
120
|
+
logger.warning('Run `hula init` to refresh skills, templates, and config.');
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
// Never let the reminder break the CLI.
|
|
125
|
+
}
|
|
126
|
+
}
|
|
96
127
|
// Check for GitHub App installation (non-intrusive, runs in background)
|
|
97
128
|
// Skip for help/version/login commands
|
|
98
129
|
if (!isLoginCommand && config.hulaApiKey) {
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA,+DAA+D;AAC/D,+CAA+C;AAC/C,MAAM,mBAAmB,GAAG,OAAO,CAAC,WAAW,CAAC;AAChD,OAAO,CAAC,WAAW,GAAG,CAAC,CAAC,OAAuB,EAAE,GAAG,IAAe,EAAE,EAAE;IACrE,IACE,OAAO,OAAO,KAAK,QAAQ;QAC3B,OAAO,CAAC,QAAQ,CAAC,8BAA8B,CAAC,EAChD,CAAC;QACD,OAAO;IACT,CAAC;IACD,IACE,OAAO,OAAO,KAAK,QAAQ;QAC3B,OAAO;QACP,MAAM,IAAI,OAAO;QAChB,OAAiC,CAAC,IAAI,KAAK,8BAA8B,EAC1E,CAAC;QACD,OAAO;IACT,CAAC;IACD,OAAQ,mBAAgC,CAAC,IAAI,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC,CAAC;AAC3E,CAAC,CAA+B,CAAC;AAEjC,gDAAgD;AAChD,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,CAAC,OAAO,EAAE,EAAE;IAChC,iDAAiD;IACjD,IACE,OAAO,CAAC,IAAI,KAAK,SAAS;QAC1B,OAAO,CAAC,OAAO,CAAC,QAAQ,CAAC,8BAA8B,CAAC,EACxD,CAAC;QACD,OAAO;IACT,CAAC;IACD,uDAAuD;IACvD,8CAA8C;AAChD,CAAC,CAAC,CAAC;AAEH,iEAAiE;AACjE,MAAM,oBAAoB,GAAG,OAAO,CAAC,KAAK,CAAC;AAC3C,OAAO,CAAC,KAAK,GAAG,CAAC,GAAG,IAAe,EAAE,EAAE;IACrC,MAAM,GAAG,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;IACpB,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,CAAC,QAAQ,CAAC,8BAA8B,CAAC,EAAE,CAAC;QAC5E,OAAO;IACT,CAAC;IACD,OAAO,oBAAoB,CAAC,KAAK,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;AACnD,CAAC,CAAC;AAEF,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -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":"AAAA,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAC1D,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,IAAI,CAAC;AACtD,OAAO,EAAE,MAAM,EAAE,MAAM,uBAAuB,CAAC;AAE/C,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AACvD,OAAO,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AACxD,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAYnD;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,OAAO,kBAAkB;IACZ,MAAM,CAAS;IAEhC,YAAY,MAAc;QACxB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACvB,CAAC;IAED;;OAEG;IACK,aAAa;QACnB,OAAO,IAAI,CAAC,MAAM,CAAC,YAAY,IAAI,MAAM,CAAC;IAC5C,CAAC;IAED;;;OAGG;IACK,cAAc,CAAC,MAAc;QACnC,IACE,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC;YACpB,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC;YACpB,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC;YACpB,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC;YACpB,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC;YACpB,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC;YACrB,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC;YACrB,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC;YACrB,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC;YACrB,MAAM,CAAC,IAAI,EAAE,KAAK,EAAE,EACpB,CAAC;YACD,MAAM,IAAI,KAAK,CAAC,0BAA0B,MAAM,GAAG,CAAC,CAAC;QACvD,CAAC;IACH,CAAC;IAED;;;OAGG;IACK,KAAK,CAAC,uBAAuB;QACnC,MAAM,WAAW,GAAG,IAAI,CAAC,MAAM,CAAC,gBAAgB,IAAI,iBAAiB,CAAC;QACtE,MAAM,QAAQ,GAAG,MAAM,WAAW,EAAE,CAAC;QACrC,OAAO,UAAU,CAAC,WAAW,CAAC;YAC5B,CAAC,CAAC,WAAW;YACb,CAAC,CAAC,OAAO,CAAC,QAAQ,EAAE,WAAW,CAAC,CAAC;IACrC,CAAC;IAED;;;;OAIG;IACK,KAAK,CAAC,YAAY,CACxB,IAAY,EACZ,MAAc,EACd,aAAqB,EACrB,SAAyC;QAEzC,IAAI,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC;QAE5B,MAAM,gBAAgB,GAAG,MAAM,IAAI,CAAC,uBAAuB,EAAE,CAAC;QAC9D,MAAM,QAAQ,GAAG,MAAM,WAAW,EAAE,CAAC;QACrC,MAAM,eAAe,GAAG,IAAI,eAAe,CAAC;YAC1C,QAAQ,EAAE,gBAAgB;YAC1B,QAAQ,EAAE,QAAQ,CAAC,IAAI;SACxB,CAAC,CAAC;QAEH,+EAA+E;QAC/E,MAAM,UAAU,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC;QACnC,MAAM,YAAY,GAAG,eAAe,CAAC,cAAc,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC;QAEtE,IAAI,eAAe,GAAG,KAAK,CAAC;QAC5B,IAAI,CAAC;YACH,MAAM,eAAe,CAAC,gBAAgB,CACpC,YAAY,EACZ,UAAU,MAAM,EAAE,EAClB,IAAI,CACL,CAAC;YACF,eAAe,GAAG,IAAI,CAAC;YAEvB,SAAS,CAAC,YAAY,CAAC,CAAC;YAExB,MAAM,UAAU,GAAG,MAAM,eAAe,CAAC,YAAY,CAAC,YAAY,CAAC,CAAC;YACpE,IAAI,UAAU,EAAE,CAAC;gBACf,MAAM,eAAe,CAAC,eAAe,CAAC,YAAY,EAAE,aAAa,CAAC,CAAC;YACrE,CAAC;iBAAM,CAAC;gBACN,MAAM,CAAC,IAAI,CAAC,6CAA6C,CAAC,CAAC;YAC7D,CAAC;YAED,2EAA2E;YAC3E,MAAM,eAAe,CAAC,MAAM,CAAC,YAAY,EAAE,QAAQ,MAAM,EAAE,CAAC,CAAC;QAC/D,CAAC;gBAAS,CAAC;YACT,IAAI,eAAe,EAAE,CAAC;gBACpB,IAAI,CAAC;oBACH,MAAM,eAAe,CAAC,gBAAgB,CAAC,YAAY,CAAC,CAAC;gBACvD,CAAC;gBAAC,OAAO,KAAK,EAAE,CAAC;oBACf,MAAM,CAAC,OAAO,CACZ,gCAAgC,YAAY,KAAK,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAC1G,CAAC;oBACF,MAAM,CAAC,IAAI,CACT,yDAAyD,YAAY,WAAW,CACjF,CAAC;gBACJ,CAAC;YACH,CAAC;QACH,CAAC;IACH,CAAC;IAED;;;;;;OAMG;IACH,KAAK,CAAC,WAAW,CACf,YAAoB,EACpB,OAAe,EACf,IAAyB;QAEzB,MAAM,MAAM,GAAG,IAAI,EAAE,MAAM,IAAI,IAAI,CAAC,aAAa,EAAE,CAAC;QACpD,MAAM,aAAa,GAAG,IAAI,EAAE,aAAa,IAAI,8BAA8B,CAAC;QAE5E,MAAM,IAAI,CAAC,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,aAAa,EAAE,CAAC,YAAY,EAAE,EAAE;YACzE,MAAM,QAAQ,GAAG,IAAI,CAAC,YAAY,EAAE,YAAY,CAAC,CAAC;YAClD,SAAS,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;YAClD,aAAa,CAAC,QAAQ,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;QAC5C,CAAC,CAAC,CAAC;IACL,CAAC;IAED;;;;;OAKG;IACH,KAAK,CAAC,UAAU,CACd,YAAoB,EACpB,IAAyB;QAEzB,MAAM,MAAM,GAAG,IAAI,EAAE,MAAM,IAAI,IAAI,CAAC,aAAa,EAAE,CAAC;QACpD,MAAM,aAAa,GAAG,IAAI,EAAE,aAAa,IAAI,6BAA6B,CAAC;QAE3E,MAAM,IAAI,CAAC,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,aAAa,EAAE,CAAC,YAAY,EAAE,EAAE;YACzE,MAAM,QAAQ,GAAG,IAAI,CAAC,YAAY,EAAE,YAAY,CAAC,CAAC;YAClD,yEAAyE;YACzE,wCAAwC;YACxC,MAAM,CAAC,QAAQ,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QACpC,CAAC,CAAC,CAAC;IACL,CAAC;CACF"}
|
|
@@ -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
|