hub-launch 1.30.0 → 1.31.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 +14 -0
- package/README.md +52 -0
- package/dist/commands/errorWatcher.d.ts +2 -0
- package/dist/commands/errorWatcher.d.ts.map +1 -1
- package/dist/commands/errorWatcher.js +13 -0
- package/dist/commands/errorWatcher.js.map +1 -1
- package/dist/commands/init.d.ts +12 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +52 -1
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/launch.d.ts +36 -0
- package/dist/commands/launch.d.ts.map +1 -1
- package/dist/commands/launch.js +267 -6
- package/dist/commands/launch.js.map +1 -1
- package/dist/commands/login.d.ts +14 -1
- package/dist/commands/login.d.ts.map +1 -1
- package/dist/commands/login.js +26 -4
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/schedule.d.ts.map +1 -1
- package/dist/commands/schedule.js +12 -0
- package/dist/commands/schedule.js.map +1 -1
- package/dist/commands/script.d.ts +1 -1
- package/dist/commands/script.d.ts.map +1 -1
- package/dist/commands/script.js +3 -0
- package/dist/commands/script.js.map +1 -1
- package/dist/commands/session-hook.d.ts +9 -0
- package/dist/commands/session-hook.d.ts.map +1 -1
- package/dist/commands/session-hook.js +60 -15
- package/dist/commands/session-hook.js.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/scripts/backlog-add.d.ts +2 -0
- package/dist/scripts/backlog-add.d.ts.map +1 -0
- package/dist/scripts/backlog-add.js +145 -0
- package/dist/scripts/backlog-add.js.map +1 -0
- package/dist/scripts/backlog-init.d.ts +2 -0
- package/dist/scripts/backlog-init.d.ts.map +1 -0
- package/dist/scripts/backlog-init.js +84 -0
- package/dist/scripts/backlog-init.js.map +1 -0
- package/dist/scripts/backlog-resync.d.ts +2 -0
- package/dist/scripts/backlog-resync.d.ts.map +1 -0
- package/dist/scripts/backlog-resync.js +156 -0
- package/dist/scripts/backlog-resync.js.map +1 -0
- package/dist/scripts/launch-run.d.ts +1 -0
- package/dist/scripts/launch-run.d.ts.map +1 -1
- package/dist/scripts/launch-run.js +47 -3
- package/dist/scripts/launch-run.js.map +1 -1
- package/dist/scripts/lib/backlog-sync.d.ts +27 -0
- package/dist/scripts/lib/backlog-sync.d.ts.map +1 -0
- package/dist/scripts/lib/backlog-sync.js +48 -0
- package/dist/scripts/lib/backlog-sync.js.map +1 -0
- package/dist/scripts/lib/read-config.d.ts +16 -0
- package/dist/scripts/lib/read-config.d.ts.map +1 -1
- package/dist/scripts/lib/read-config.js +34 -0
- package/dist/scripts/lib/read-config.js.map +1 -1
- package/dist/services/api/HulaApiClient.d.ts +39 -0
- package/dist/services/api/HulaApiClient.d.ts.map +1 -1
- package/dist/services/api/HulaApiClient.js +15 -0
- package/dist/services/api/HulaApiClient.js.map +1 -1
- package/dist/services/github/IssueService.d.ts.map +1 -1
- package/dist/services/github/IssueService.js +3 -0
- package/dist/services/github/IssueService.js.map +1 -1
- package/dist/services/plan/GitHubSourcePlanService.d.ts +67 -0
- package/dist/services/plan/GitHubSourcePlanService.d.ts.map +1 -0
- package/dist/services/plan/GitHubSourcePlanService.js +148 -0
- package/dist/services/plan/GitHubSourcePlanService.js.map +1 -0
- package/dist/services/plan/PlanBranchService.d.ts +5 -1
- package/dist/services/plan/PlanBranchService.d.ts.map +1 -1
- package/dist/services/plan/PlanBranchService.js +21 -3
- package/dist/services/plan/PlanBranchService.js.map +1 -1
- package/dist/templates/proceed-instructions.md +2 -1
- package/dist/templates/skills/hula-approve/SKILL.md +31 -0
- package/dist/templates/skills/hula-fix/SKILL.md +10 -0
- package/dist/templates/skills/hula-launch/SKILL.md +40 -1
- package/dist/templates/skills/hula-plan/SKILL.md +39 -4
- package/dist/types/config.schema.d.ts +24 -0
- package/dist/types/config.schema.d.ts.map +1 -1
- package/dist/types/config.schema.js +11 -0
- package/dist/types/config.schema.js.map +1 -1
- package/dist/types/issue.schema.d.ts +6 -0
- package/dist/types/issue.schema.d.ts.map +1 -1
- package/dist/types/issue.schema.js +8 -0
- package/dist/types/issue.schema.js.map +1 -1
- package/dist/types/pr.schema.d.ts +38 -0
- package/dist/types/pr.schema.d.ts.map +1 -1
- package/dist/types/pr.schema.js +7 -0
- package/dist/types/pr.schema.js.map +1 -1
- package/dist/utils/client-session.d.ts +16 -0
- package/dist/utils/client-session.d.ts.map +1 -1
- package/dist/utils/client-session.js +29 -0
- package/dist/utils/client-session.js.map +1 -1
- package/dist/utils/sandbox-profile.d.ts +44 -0
- package/dist/utils/sandbox-profile.d.ts.map +1 -0
- package/dist/utils/sandbox-profile.js +78 -0
- package/dist/utils/sandbox-profile.js.map +1 -0
- package/package.json +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Command } from 'commander';
|
|
2
2
|
import type { Config } from '../types/index.js';
|
|
3
3
|
import type { LlmProviderType } from '../types/config.schema.js';
|
|
4
|
+
import { type SandboxProfile } from '../utils/sandbox-profile.js';
|
|
4
5
|
/**
|
|
5
6
|
* Launch command - Trigger a launch job on hula-project server
|
|
6
7
|
*
|
|
@@ -30,6 +31,12 @@ interface LaunchOptions {
|
|
|
30
31
|
verbose?: boolean;
|
|
31
32
|
test?: boolean;
|
|
32
33
|
handoff?: string;
|
|
34
|
+
/** --issue: existing GitHub issue reference (number, #number, or URL). */
|
|
35
|
+
issue?: string;
|
|
36
|
+
/** --pr: existing pull request reference (number, #number, or URL). */
|
|
37
|
+
pr?: string;
|
|
38
|
+
/** --base: branch to cut the work branch from and open the PR into. */
|
|
39
|
+
base?: string;
|
|
33
40
|
show?: string;
|
|
34
41
|
logs?: string;
|
|
35
42
|
lines?: number;
|
|
@@ -42,6 +49,8 @@ interface LaunchOptions {
|
|
|
42
49
|
containerCpu?: number;
|
|
43
50
|
containerMemory?: number;
|
|
44
51
|
containerDisk?: number;
|
|
52
|
+
/** --sandbox-profile: base | android[:17|21]; overrides config.sandboxProfile. */
|
|
53
|
+
sandboxProfile?: string;
|
|
45
54
|
updateNotificationUrl?: string;
|
|
46
55
|
updateNotificationNameTag?: string;
|
|
47
56
|
/** Parent folder containing sibling repos to fan out over. */
|
|
@@ -79,6 +88,19 @@ export declare function validateKillFlags(options: LaunchOptions): string | null
|
|
|
79
88
|
* `--kill` needs only the tracking name; every other mode needs a plan.
|
|
80
89
|
*/
|
|
81
90
|
export declare function requiresPlanPath(options: LaunchOptions): boolean;
|
|
91
|
+
/**
|
|
92
|
+
* Validate the `--issue` / `--pr` / `--base` flag matrix.
|
|
93
|
+
*
|
|
94
|
+
* - A plan file may accompany `--issue` OR `--pr`, but not both — with both and
|
|
95
|
+
* a plan file there is no single instruction source. Without a plan file,
|
|
96
|
+
* `--issue` and `--pr` combine (plan generated from both).
|
|
97
|
+
* - `--base` is meaningless with `--pr` (the base comes from the PR itself).
|
|
98
|
+
* - `--issue` / `--pr` are single-repo launch flows, incompatible with the
|
|
99
|
+
* read/group/kill flags in {@link SOURCE_INCOMPATIBLE_FLAGS}.
|
|
100
|
+
*
|
|
101
|
+
* Returns an error message when the combination is invalid, or `null` when OK.
|
|
102
|
+
*/
|
|
103
|
+
export declare function validateSourceFlags(options: LaunchOptions, hasPlanPathPositional: boolean): string | null;
|
|
82
104
|
/**
|
|
83
105
|
* Validate the multi-repo `--folder` / `--group-id` / `--plan` flag matrix.
|
|
84
106
|
*
|
|
@@ -175,6 +197,8 @@ export interface RalphRunBodyInput {
|
|
|
175
197
|
memory: number;
|
|
176
198
|
disk: number;
|
|
177
199
|
};
|
|
200
|
+
/** Non-base sandbox profile; omitted for base (the server default). */
|
|
201
|
+
sandboxProfile?: SandboxProfile;
|
|
178
202
|
updateNotificationUrl?: string;
|
|
179
203
|
updateNotificationNameTag?: string;
|
|
180
204
|
githubToken?: string;
|
|
@@ -185,6 +209,18 @@ export interface RalphRunBodyInput {
|
|
|
185
209
|
* branch in the sandbox instead of creating one from the base branch.
|
|
186
210
|
*/
|
|
187
211
|
branchExists?: boolean;
|
|
212
|
+
/**
|
|
213
|
+
* Launch-from-existing linkage (optional; omitted when absent so legacy
|
|
214
|
+
* bodies stay byte-identical). An older server ignores unknown fields.
|
|
215
|
+
*/
|
|
216
|
+
/** Existing GitHub issue to link (`Closes #N`). */
|
|
217
|
+
issueNumber?: number;
|
|
218
|
+
/** Existing pull request being continued. */
|
|
219
|
+
prNumber?: number;
|
|
220
|
+
/** The PR's head branch (the work branch; may contain `/`, unsanitized). */
|
|
221
|
+
headBranch?: string;
|
|
222
|
+
/** The PR's / --base target branch the PR opens into. */
|
|
223
|
+
baseBranch?: string;
|
|
188
224
|
}
|
|
189
225
|
/**
|
|
190
226
|
* Parse a beforeLaunch hook's stdout into an envVars object to merge into the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"launch.d.ts","sourceRoot":"","sources":["../../src/commands/launch.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAUzC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"launch.d.ts","sourceRoot":"","sources":["../../src/commands/launch.ts"],"names":[],"mappings":"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"}
|
package/dist/commands/launch.js
CHANGED
|
@@ -14,6 +14,7 @@ import { GroupLaunchService, validateGroupId, } from '../services/group/GroupLau
|
|
|
14
14
|
import { TempFileService } from '../services/logs/TempFileService.js';
|
|
15
15
|
import { EditorService } from '../services/editor/EditorService.js';
|
|
16
16
|
import { ensurePlanBranch, PlanNotFoundError, } from '../services/plan/PlanBranchService.js';
|
|
17
|
+
import { parseGitHubRef, findLinkedIssueNumber, resolveIssueSource, resolvePrSource, buildPlanMarkdown, writeGeneratedPlan, } from '../services/plan/GitHubSourcePlanService.js';
|
|
17
18
|
import { loadGitHubToken } from './login.js';
|
|
18
19
|
import { resolveProject } from '../utils/project-resolver.js';
|
|
19
20
|
import { HULA_PROJECT_URL } from '../config/constants.js';
|
|
@@ -25,6 +26,7 @@ import { findGitRoot } from '../utils/git-utils.js';
|
|
|
25
26
|
import { upsertProviderApiKey } from '../utils/config-file-edit.js';
|
|
26
27
|
import { validateNoReservedVars, readAndFilterEnvVars, RESERVED_ENV_VARS, } from '../utils/env-vars.js';
|
|
27
28
|
import { HookExecutor } from '../services/hooks/HookExecutor.js';
|
|
29
|
+
import { describeSandboxProfile, resolveSandboxProfile, sandboxProfileForRequest, } from '../utils/sandbox-profile.js';
|
|
28
30
|
const DEFAULT_CONTAINER_RESOURCES = { cpu: 2, memory: 4, disk: 10 };
|
|
29
31
|
/**
|
|
30
32
|
* Launch command - Trigger a launch job on hula-project server
|
|
@@ -69,11 +71,15 @@ export function launchCommand(program, config) {
|
|
|
69
71
|
// 'Run in test mode (server uses a mock Claude — fast E2E run, still creates a real PR)',
|
|
70
72
|
// )
|
|
71
73
|
.option('--handoff <username>', 'Assign the created issue to this GitHub user')
|
|
74
|
+
.option('--issue <ref>', 'Work on an existing GitHub issue (number, #number, or URL). Never creates a new issue. A single positional is the tracking name, not a plan path.')
|
|
75
|
+
.option('--pr <ref>', 'Continue an existing pull request in this repository (number, #number, or URL). Pushes to its branch. A single positional is the tracking name, not a plan path.')
|
|
76
|
+
.option('--base <branch>', 'Branch to cut the new work branch from and open the PR into (default: the repository default branch)')
|
|
72
77
|
.option('--provider <type>', 'LLM provider: claude | openai | openrouter (default: config or claude)')
|
|
73
78
|
.option('--provider-key <key>', 'Provider credential (ephemeral; never stored server-side beyond the run)')
|
|
74
79
|
.option('--container-cpu <n>', 'vCPU cores for the sandbox (1–32)', parseInt)
|
|
75
80
|
.option('--container-memory <n>', 'GiB RAM for the sandbox (1–128)', parseInt)
|
|
76
81
|
.option('--container-disk <n>', 'GiB disk for the sandbox (5–200)', parseInt)
|
|
82
|
+
.option('--sandbox-profile <profile>', 'Sandbox image: base | android | android:17 | android:21 (default: config sandboxProfile, else base)')
|
|
77
83
|
.option('--update-notification-url <url>', 'Webhook URL to POST a notification to on every terminal task transition')
|
|
78
84
|
.option('--update-notification-name-tag <tag>', 'Verbatim "initiated by" label for notifications (Slack mention markup pings). Falls back to config then HULA_UPDATE_NOTIFICATION_NAME_TAG.')
|
|
79
85
|
.option('--show <name>', 'Show job status for the specified issue')
|
|
@@ -121,6 +127,13 @@ export function launchCommand(program, config) {
|
|
|
121
127
|
logger.error(folderFlagError);
|
|
122
128
|
process.exit(1);
|
|
123
129
|
}
|
|
130
|
+
// Validate the launch-from-existing `--issue` / `--pr` / `--base`
|
|
131
|
+
// flag combination before any work.
|
|
132
|
+
const sourceFlagError = validateSourceFlags(options, Boolean(planPath));
|
|
133
|
+
if (sourceFlagError) {
|
|
134
|
+
logger.error(sourceFlagError);
|
|
135
|
+
process.exit(1);
|
|
136
|
+
}
|
|
124
137
|
// Validate an explicitly-supplied group id against the server format.
|
|
125
138
|
if (typeof options.groupId === 'string') {
|
|
126
139
|
const groupIdError = validateGroupId(options.groupId);
|
|
@@ -162,6 +175,13 @@ export function launchCommand(program, config) {
|
|
|
162
175
|
await executeKill(config, issueName, options);
|
|
163
176
|
return;
|
|
164
177
|
}
|
|
178
|
+
// With --issue / --pr the plan (and tracking name) are derived from
|
|
179
|
+
// GitHub, so neither positional is required. A single positional is
|
|
180
|
+
// the tracking name; the plan path is generated.
|
|
181
|
+
if (options.issue || options.pr) {
|
|
182
|
+
await executeLaunch(config, issueName, planPath, options);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
165
185
|
// Regular launch / --kill-and-relaunch requires issueName and planPath
|
|
166
186
|
if (!issueName || !planPath) {
|
|
167
187
|
logger.error('Usage: hula launch <issueName> <planPath>');
|
|
@@ -217,7 +237,52 @@ export function validateKillFlags(options) {
|
|
|
217
237
|
* `--kill` needs only the tracking name; every other mode needs a plan.
|
|
218
238
|
*/
|
|
219
239
|
export function requiresPlanPath(options) {
|
|
220
|
-
return !options.kill;
|
|
240
|
+
return !options.kill && !options.issue && !options.pr;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Launch-only flags that cannot be combined with `--issue` / `--pr`. These are
|
|
244
|
+
* either read/group modes or the kill flows, none of which make sense when
|
|
245
|
+
* starting from an existing issue or PR (single-repo launch only).
|
|
246
|
+
*/
|
|
247
|
+
const SOURCE_INCOMPATIBLE_FLAGS = [
|
|
248
|
+
{ key: 'folder', flag: '--folder' },
|
|
249
|
+
{ key: 'groupId', flag: '--group-id' },
|
|
250
|
+
{ key: 'kill', flag: '--kill' },
|
|
251
|
+
{ key: 'killAndRelaunch', flag: '--kill-and-relaunch' },
|
|
252
|
+
{ key: 'show', flag: '--show' },
|
|
253
|
+
{ key: 'logs', flag: '--logs' },
|
|
254
|
+
{ key: 'showGroup', flag: '--show-group' },
|
|
255
|
+
];
|
|
256
|
+
/**
|
|
257
|
+
* Validate the `--issue` / `--pr` / `--base` flag matrix.
|
|
258
|
+
*
|
|
259
|
+
* - A plan file may accompany `--issue` OR `--pr`, but not both — with both and
|
|
260
|
+
* a plan file there is no single instruction source. Without a plan file,
|
|
261
|
+
* `--issue` and `--pr` combine (plan generated from both).
|
|
262
|
+
* - `--base` is meaningless with `--pr` (the base comes from the PR itself).
|
|
263
|
+
* - `--issue` / `--pr` are single-repo launch flows, incompatible with the
|
|
264
|
+
* read/group/kill flags in {@link SOURCE_INCOMPATIBLE_FLAGS}.
|
|
265
|
+
*
|
|
266
|
+
* Returns an error message when the combination is invalid, or `null` when OK.
|
|
267
|
+
*/
|
|
268
|
+
export function validateSourceFlags(options, hasPlanPathPositional) {
|
|
269
|
+
const hasIssue = Boolean(options.issue);
|
|
270
|
+
const hasPr = Boolean(options.pr);
|
|
271
|
+
if (hasPlanPathPositional && hasIssue && hasPr) {
|
|
272
|
+
return 'Pass a plan file with --issue or with --pr, not with both. Without a plan file, --issue and --pr can be combined.';
|
|
273
|
+
}
|
|
274
|
+
if (hasPr && options.base) {
|
|
275
|
+
return '--base cannot be used with --pr: the base branch comes from the pull request';
|
|
276
|
+
}
|
|
277
|
+
if (hasIssue || hasPr) {
|
|
278
|
+
for (const { key, flag } of SOURCE_INCOMPATIBLE_FLAGS) {
|
|
279
|
+
const value = options[key];
|
|
280
|
+
if (value !== undefined && value !== false) {
|
|
281
|
+
return `--issue/--pr cannot be combined with ${flag}`;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return null;
|
|
221
286
|
}
|
|
222
287
|
/**
|
|
223
288
|
* Validate the multi-repo `--folder` / `--group-id` / `--plan` flag matrix.
|
|
@@ -720,6 +785,11 @@ export function buildRalphRunRequestBody(input) {
|
|
|
720
785
|
if (input.containerResources) {
|
|
721
786
|
body.containerResources = input.containerResources;
|
|
722
787
|
}
|
|
788
|
+
// Maps to RalphRunRequest.sandboxProfile. Only non-base profiles are sent so
|
|
789
|
+
// base launches stay byte-identical (see sandboxProfileForRequest).
|
|
790
|
+
if (input.sandboxProfile) {
|
|
791
|
+
body.sandboxProfile = input.sandboxProfile;
|
|
792
|
+
}
|
|
723
793
|
if (input.updateNotificationUrl) {
|
|
724
794
|
body.updateNotificationUrl = input.updateNotificationUrl;
|
|
725
795
|
}
|
|
@@ -737,8 +807,69 @@ export function buildRalphRunRequestBody(input) {
|
|
|
737
807
|
if (input.branchExists) {
|
|
738
808
|
body.branchExists = true;
|
|
739
809
|
}
|
|
810
|
+
// Launch-from-existing linkage (appended after branchExists, each only when
|
|
811
|
+
// defined, so non-source launches stay byte-identical). An older server
|
|
812
|
+
// ignores these unknown fields.
|
|
813
|
+
if (input.issueNumber !== undefined) {
|
|
814
|
+
body.issueNumber = input.issueNumber;
|
|
815
|
+
}
|
|
816
|
+
if (input.prNumber !== undefined) {
|
|
817
|
+
body.prNumber = input.prNumber;
|
|
818
|
+
}
|
|
819
|
+
if (input.headBranch !== undefined) {
|
|
820
|
+
body.headBranch = input.headBranch;
|
|
821
|
+
}
|
|
822
|
+
if (input.baseBranch !== undefined) {
|
|
823
|
+
body.baseBranch = input.baseBranch;
|
|
824
|
+
}
|
|
740
825
|
return body;
|
|
741
826
|
}
|
|
827
|
+
/**
|
|
828
|
+
* True when `msg` is one of the deliberate validation errors thrown by
|
|
829
|
+
* {@link resolveIssueSource} / {@link resolvePrSource} (closed, merged, fork,
|
|
830
|
+
* wrong kind) — those must pass through verbatim, not be re-wrapped as a fetch
|
|
831
|
+
* failure.
|
|
832
|
+
*/
|
|
833
|
+
function isGitHubSourceValidationError(msg) {
|
|
834
|
+
return /is a pull request, not an issue|is closed\. Reopen|is already merged|comes from a fork/.test(msg);
|
|
835
|
+
}
|
|
836
|
+
/**
|
|
837
|
+
* {@link resolveIssueSource} with the GitHub-fetch error mapping from the plan's
|
|
838
|
+
* edge cases: 404 → a clear "not found in <owner/repo>", any other fetch error
|
|
839
|
+
* → "Could not fetch issue #<n>: …". Validation errors pass through unchanged.
|
|
840
|
+
*/
|
|
841
|
+
async function resolveIssueSourceWrapped(n, repo, repoFullName) {
|
|
842
|
+
try {
|
|
843
|
+
return await resolveIssueSource(n, repo);
|
|
844
|
+
}
|
|
845
|
+
catch (err) {
|
|
846
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
847
|
+
if (isGitHubSourceValidationError(msg))
|
|
848
|
+
throw err;
|
|
849
|
+
if (/404|not found/i.test(msg)) {
|
|
850
|
+
throw new Error(`Issue #${n} not found in ${repoFullName}`);
|
|
851
|
+
}
|
|
852
|
+
throw new Error(`Could not fetch issue #${n}: ${msg}`);
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
/**
|
|
856
|
+
* {@link resolvePrSource} with the same GitHub-fetch error mapping as
|
|
857
|
+
* {@link resolveIssueSourceWrapped}.
|
|
858
|
+
*/
|
|
859
|
+
async function resolvePrSourceWrapped(n, repoFullName) {
|
|
860
|
+
try {
|
|
861
|
+
return await resolvePrSource(n);
|
|
862
|
+
}
|
|
863
|
+
catch (err) {
|
|
864
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
865
|
+
if (isGitHubSourceValidationError(msg))
|
|
866
|
+
throw err;
|
|
867
|
+
if (/404|not found/i.test(msg)) {
|
|
868
|
+
throw new Error(`PR #${n} not found in ${repoFullName}`);
|
|
869
|
+
}
|
|
870
|
+
throw new Error(`Could not fetch PR #${n}: ${msg}`);
|
|
871
|
+
}
|
|
872
|
+
}
|
|
742
873
|
/**
|
|
743
874
|
* Single-repo launch entry point. Runs the plan-sync/upload check, then defers
|
|
744
875
|
* the body-build + POST to {@link launchSingleRepo} in single mode (keeps the
|
|
@@ -776,11 +907,121 @@ conflictModeOverride) {
|
|
|
776
907
|
* @param options - Parsed CLI options.
|
|
777
908
|
* @param ctx - Mode + shared-once context (see {@link SingleRepoLaunchContext}).
|
|
778
909
|
*/
|
|
779
|
-
async function launchSingleRepo(repoConfig, repoRoot,
|
|
910
|
+
async function launchSingleRepo(repoConfig, repoRoot, issueNameInput, planPathInput, options, ctx) {
|
|
780
911
|
const isGroup = ctx.mode === 'group';
|
|
781
912
|
// Effective conflict mode: an explicit override (from the 409 resolver) or the
|
|
782
913
|
// --kill-and-relaunch flag. Undefined = default 'reject' (byte-identical body).
|
|
783
914
|
const conflictMode = deriveConflictMode(options, ctx.conflictModeOverride);
|
|
915
|
+
// ── Launch-from-existing resolution (FR 8) ────────────────────────────────
|
|
916
|
+
// With --issue / --pr the tracking name and plan are derived from GitHub, and
|
|
917
|
+
// --base / --pr change the branch the plan is published to and the PR base.
|
|
918
|
+
// Resolve all of that here, before ensurePlanBranch, so the proven publish
|
|
919
|
+
// path is reused with overrides and the request body gains the linkage fields.
|
|
920
|
+
let resolvedIssueName = issueNameInput;
|
|
921
|
+
let resolvedPlanPath = planPathInput;
|
|
922
|
+
let planBranchOpts;
|
|
923
|
+
const sourceBody = {};
|
|
924
|
+
if (options.issue || options.pr || options.base) {
|
|
925
|
+
try {
|
|
926
|
+
const repoFullName = ctx.repositoryId ??
|
|
927
|
+
(await resolveProject(repoConfig, undefined, repoRoot));
|
|
928
|
+
const [repoOwner, repoName] = repoFullName.split('/');
|
|
929
|
+
// Reject a URL that points at a different repository than this one.
|
|
930
|
+
const assertSameRepo = (ref, kind) => {
|
|
931
|
+
if (ref.owner &&
|
|
932
|
+
ref.repo &&
|
|
933
|
+
(ref.owner.toLowerCase() !== (repoOwner ?? '').toLowerCase() ||
|
|
934
|
+
ref.repo.toLowerCase() !== (repoName ?? '').toLowerCase())) {
|
|
935
|
+
throw new Error(`--${kind} URL points to ${ref.owner}/${ref.repo}, but this repository is ${repoOwner}/${repoName}`);
|
|
936
|
+
}
|
|
937
|
+
};
|
|
938
|
+
let pr;
|
|
939
|
+
let issue;
|
|
940
|
+
if (options.pr) {
|
|
941
|
+
const prRef = parseGitHubRef(options.pr, 'pr');
|
|
942
|
+
assertSameRepo(prRef, 'pr');
|
|
943
|
+
pr = await resolvePrSourceWrapped(prRef.number, repoFullName);
|
|
944
|
+
// Linked issue: an explicit --issue wins; otherwise auto-detect from the
|
|
945
|
+
// PR body and drop a closed linked issue with a warning.
|
|
946
|
+
if (options.issue) {
|
|
947
|
+
const issueRef = parseGitHubRef(options.issue, 'issue');
|
|
948
|
+
assertSameRepo(issueRef, 'issue');
|
|
949
|
+
issue = await resolveIssueSourceWrapped(issueRef.number, { owner: issueRef.owner, name: issueRef.repo }, repoFullName);
|
|
950
|
+
}
|
|
951
|
+
else {
|
|
952
|
+
const linked = findLinkedIssueNumber(pr.body);
|
|
953
|
+
if (linked !== null) {
|
|
954
|
+
try {
|
|
955
|
+
issue = await resolveIssueSourceWrapped(linked, {}, repoFullName);
|
|
956
|
+
}
|
|
957
|
+
catch (linkedErr) {
|
|
958
|
+
const linkedMsg = linkedErr instanceof Error
|
|
959
|
+
? linkedErr.message
|
|
960
|
+
: String(linkedErr);
|
|
961
|
+
if (linkedMsg.includes('is closed')) {
|
|
962
|
+
logger.warning(`Linked issue #${linked} is closed — continuing without linking it`);
|
|
963
|
+
}
|
|
964
|
+
else {
|
|
965
|
+
throw linkedErr;
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
else if (options.issue) {
|
|
972
|
+
const issueRef = parseGitHubRef(options.issue, 'issue');
|
|
973
|
+
assertSameRepo(issueRef, 'issue');
|
|
974
|
+
issue = await resolveIssueSourceWrapped(issueRef.number, { owner: issueRef.owner, name: issueRef.repo }, repoFullName);
|
|
975
|
+
}
|
|
976
|
+
// Tracking name: the positional if given, else pr-<n> / issue-<n>.
|
|
977
|
+
if (!resolvedIssueName) {
|
|
978
|
+
resolvedIssueName = pr
|
|
979
|
+
? `pr-${pr.number}`
|
|
980
|
+
: issue
|
|
981
|
+
? `issue-${issue.number}`
|
|
982
|
+
: resolvedIssueName;
|
|
983
|
+
}
|
|
984
|
+
// Plan path: the positional if given, else a generated plan from the
|
|
985
|
+
// GitHub text (only reachable with --issue / --pr; --base alone still
|
|
986
|
+
// requires a positional plan because requiresPlanPath stays true).
|
|
987
|
+
if (!resolvedPlanPath && (pr || issue)) {
|
|
988
|
+
const slug = pr ? `pr-${pr.number}` : `issue-${issue.number}`;
|
|
989
|
+
const markdown = buildPlanMarkdown({ issue, pr, repoFullName });
|
|
990
|
+
resolvedPlanPath = writeGeneratedPlan(repoConfig.planPath, slug, markdown, repoRoot);
|
|
991
|
+
logger.info(`Generated plan from ${pr ? `PR #${pr.number}` : `issue #${issue.number}`}: ${resolvedPlanPath}`);
|
|
992
|
+
}
|
|
993
|
+
// Branch overrides for ensurePlanBranch.
|
|
994
|
+
if (pr) {
|
|
995
|
+
planBranchOpts = { branch: pr.head.ref, requireExistingBranch: true };
|
|
996
|
+
}
|
|
997
|
+
else if (options.base) {
|
|
998
|
+
planBranchOpts = { baseBranch: options.base };
|
|
999
|
+
}
|
|
1000
|
+
// Request-body linkage fields.
|
|
1001
|
+
if (pr) {
|
|
1002
|
+
sourceBody.prNumber = pr.number;
|
|
1003
|
+
sourceBody.headBranch = pr.head.ref;
|
|
1004
|
+
sourceBody.baseBranch = pr.base.ref;
|
|
1005
|
+
}
|
|
1006
|
+
else if (options.base) {
|
|
1007
|
+
sourceBody.baseBranch = options.base;
|
|
1008
|
+
}
|
|
1009
|
+
if (issue) {
|
|
1010
|
+
sourceBody.issueNumber = issue.number;
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
catch (sourceErr) {
|
|
1014
|
+
const msg = sourceErr instanceof Error ? sourceErr.message : String(sourceErr);
|
|
1015
|
+
logger.error(msg);
|
|
1016
|
+
if (isGroup)
|
|
1017
|
+
return { ok: false, message: msg };
|
|
1018
|
+
process.exit(1);
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
// After resolution both are guaranteed: the handler requires positionals
|
|
1022
|
+
// except with --issue / --pr, which populate them above.
|
|
1023
|
+
const issueName = resolvedIssueName;
|
|
1024
|
+
const planPath = resolvedPlanPath;
|
|
784
1025
|
// Plan-branch flow: guarantee the feature branch exists on origin with the
|
|
785
1026
|
// current plan content BEFORE anything else — the beforeLaunch hook below
|
|
786
1027
|
// may register branch-scoped external resources (e.g. Vercel Preview env
|
|
@@ -791,7 +1032,7 @@ async function launchSingleRepo(repoConfig, repoRoot, issueName, planPath, optio
|
|
|
791
1032
|
// that only exist on origin/main.
|
|
792
1033
|
let planBranch;
|
|
793
1034
|
try {
|
|
794
|
-
const ensured = await ensurePlanBranch(repoConfig, planPath, issueName, repoRoot);
|
|
1035
|
+
const ensured = await ensurePlanBranch(repoConfig, planPath, issueName, repoRoot, planBranchOpts);
|
|
795
1036
|
planBranch = ensured.branch;
|
|
796
1037
|
if (ensured.created) {
|
|
797
1038
|
logger.success(`Branch '${ensured.branch}' created on origin with the plan`);
|
|
@@ -1009,6 +1250,18 @@ async function launchSingleRepo(repoConfig, repoRoot, issueName, planPath, optio
|
|
|
1009
1250
|
containerResources = { cpu, memory, disk };
|
|
1010
1251
|
logger.log(` Container Resources: cpu=${cpu} memory=${memory}GiB disk=${disk}GiB`);
|
|
1011
1252
|
}
|
|
1253
|
+
// Resolve the sandbox profile: --sandbox-profile, then config, then base.
|
|
1254
|
+
const profileResult = resolveSandboxProfile(options.sandboxProfile, repoConfig.sandboxProfile);
|
|
1255
|
+
if (profileResult.error) {
|
|
1256
|
+
logger.error(profileResult.error);
|
|
1257
|
+
if (isGroup)
|
|
1258
|
+
return { ok: false, message: profileResult.error };
|
|
1259
|
+
process.exit(1);
|
|
1260
|
+
}
|
|
1261
|
+
const sandboxProfile = sandboxProfileForRequest(profileResult.profile);
|
|
1262
|
+
if (sandboxProfile) {
|
|
1263
|
+
logger.log(` Sandbox Profile: ${describeSandboxProfile(sandboxProfile)}`);
|
|
1264
|
+
}
|
|
1012
1265
|
// Resolve the GitHub token: shared (group mode) or loaded (single mode).
|
|
1013
1266
|
const githubToken = ctx.skipTokenLoad
|
|
1014
1267
|
? ctx.githubToken
|
|
@@ -1124,6 +1377,7 @@ async function launchSingleRepo(repoConfig, repoRoot, issueName, planPath, optio
|
|
|
1124
1377
|
clientSessionId: clientSessionId ?? undefined,
|
|
1125
1378
|
provider: { type: providerType, authToken },
|
|
1126
1379
|
containerResources,
|
|
1380
|
+
sandboxProfile,
|
|
1127
1381
|
updateNotificationUrl,
|
|
1128
1382
|
updateNotificationNameTag,
|
|
1129
1383
|
githubToken,
|
|
@@ -1132,6 +1386,11 @@ async function launchSingleRepo(repoConfig, repoRoot, issueName, planPath, optio
|
|
|
1132
1386
|
// plan committed) exists on origin, so the server must check out the
|
|
1133
1387
|
// existing branch instead of creating one from the base branch.
|
|
1134
1388
|
branchExists: true,
|
|
1389
|
+
// Launch-from-existing linkage (undefined unless --issue / --pr / --base).
|
|
1390
|
+
issueNumber: sourceBody.issueNumber,
|
|
1391
|
+
prNumber: sourceBody.prNumber,
|
|
1392
|
+
headBranch: sourceBody.headBranch,
|
|
1393
|
+
baseBranch: sourceBody.baseBranch,
|
|
1135
1394
|
});
|
|
1136
1395
|
try {
|
|
1137
1396
|
const response = await axios.post(endpoint, requestBody, {
|
|
@@ -1149,13 +1408,15 @@ async function launchSingleRepo(repoConfig, repoRoot, issueName, planPath, optio
|
|
|
1149
1408
|
}
|
|
1150
1409
|
logger.success(response.data.message);
|
|
1151
1410
|
logger.blank();
|
|
1411
|
+
// Link to the dashboard of the server we launched on, so a preview or
|
|
1412
|
+
// self-hosted launch is not sent to hublaunch.site.
|
|
1413
|
+
const dashboardUrl = `${serverUrl.replace(/\/$/, '')}/dashboard`;
|
|
1152
1414
|
if (response.data.planId) {
|
|
1153
|
-
const planUrl = `https://www.hublaunch.site/dashboard/plans/${response.data.planId}`;
|
|
1154
1415
|
logger.info(`Plan ID: ${response.data.planId}`);
|
|
1155
|
-
logger.info(`Track it: ${
|
|
1416
|
+
logger.info(`Track it: ${dashboardUrl}/plans/${response.data.planId}`);
|
|
1156
1417
|
}
|
|
1157
1418
|
else {
|
|
1158
|
-
logger.info(
|
|
1419
|
+
logger.info(`Track it: ${dashboardUrl}`);
|
|
1159
1420
|
}
|
|
1160
1421
|
if (!updateNotificationUrl) {
|
|
1161
1422
|
logger.info('Set updateNotificationUrl in your local hublaunch.config.js file to be able to get update messages about the status.');
|