hub-launch 1.20.0 → 1.22.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 +22 -0
- package/README.md +69 -14
- package/dist/commands/init.d.ts +21 -2
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +121 -38
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/launch.d.ts +91 -0
- package/dist/commands/launch.d.ts.map +1 -1
- package/dist/commands/launch.js +885 -144
- package/dist/commands/launch.js.map +1 -1
- package/dist/commands/login.d.ts +38 -0
- package/dist/commands/login.d.ts.map +1 -1
- package/dist/commands/login.js +107 -11
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/upload.d.ts +9 -2
- package/dist/commands/upload.d.ts.map +1 -1
- package/dist/commands/upload.js +16 -6
- package/dist/commands/upload.js.map +1 -1
- package/dist/scripts/launch-run.d.ts +6 -0
- package/dist/scripts/launch-run.d.ts.map +1 -1
- package/dist/scripts/launch-run.js +70 -9
- package/dist/scripts/launch-run.js.map +1 -1
- package/dist/services/api/HulaApiClient.d.ts +53 -0
- package/dist/services/api/HulaApiClient.d.ts.map +1 -1
- package/dist/services/api/HulaApiClient.js +18 -0
- package/dist/services/api/HulaApiClient.js.map +1 -1
- package/dist/services/git/FilePublishService.d.ts +9 -1
- package/dist/services/git/FilePublishService.d.ts.map +1 -1
- package/dist/services/git/FilePublishService.js +13 -3
- package/dist/services/git/FilePublishService.js.map +1 -1
- package/dist/services/git/GitService.d.ts +19 -0
- package/dist/services/git/GitService.d.ts.map +1 -1
- package/dist/services/git/GitService.js +56 -31
- package/dist/services/git/GitService.js.map +1 -1
- package/dist/services/git/WorktreeService.d.ts +9 -0
- package/dist/services/git/WorktreeService.d.ts.map +1 -1
- package/dist/services/git/WorktreeService.js +6 -3
- package/dist/services/git/WorktreeService.js.map +1 -1
- package/dist/services/group/GroupLaunchService.d.ts +230 -0
- package/dist/services/group/GroupLaunchService.d.ts.map +1 -0
- package/dist/services/group/GroupLaunchService.js +282 -0
- package/dist/services/group/GroupLaunchService.js.map +1 -0
- package/dist/templates/skills/hula-help/SKILL.md +1 -1
- package/dist/templates/skills/hula-launch/SKILL.md +38 -1
- package/dist/templates/skills/hula-schedule/SKILL.md +3 -3
- package/dist/utils/config-file-edit.d.ts +23 -0
- package/dist/utils/config-file-edit.d.ts.map +1 -0
- package/dist/utils/config-file-edit.js +37 -0
- package/dist/utils/config-file-edit.js.map +1 -0
- package/dist/utils/ephemeral-credentials.d.ts +8 -0
- package/dist/utils/ephemeral-credentials.d.ts.map +1 -1
- package/dist/utils/ephemeral-credentials.js +2 -1
- package/dist/utils/ephemeral-credentials.js.map +1 -1
- package/dist/utils/github-cli.d.ts +1 -1
- package/dist/utils/github-cli.d.ts.map +1 -1
- package/dist/utils/github-cli.js +15 -7
- package/dist/utils/github-cli.js.map +1 -1
- package/dist/utils/project-resolver.d.ts +1 -1
- package/dist/utils/project-resolver.d.ts.map +1 -1
- package/dist/utils/project-resolver.js +2 -2
- package/dist/utils/project-resolver.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { Config } from '../../types/index.js';
|
|
3
|
+
/** The server's `groupId` validation regex (hula-server PR #505). */
|
|
4
|
+
export declare const GROUP_ID_REGEX: RegExp;
|
|
5
|
+
/**
|
|
6
|
+
* A repository discovered under a `--folder` parent that qualifies for a group
|
|
7
|
+
* launch (has both a `.git` entry and a `.hublaunch` config file).
|
|
8
|
+
*/
|
|
9
|
+
export interface DiscoveredRepo {
|
|
10
|
+
/** The directory name (used as the `<repoName>` key for `--plan` overrides). */
|
|
11
|
+
name: string;
|
|
12
|
+
/** Absolute path to the repository root. */
|
|
13
|
+
path: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* A subdirectory that was inspected during discovery but did not qualify, with
|
|
17
|
+
* the human-readable reason (for display in the skipped list).
|
|
18
|
+
*/
|
|
19
|
+
export interface SkippedRepo {
|
|
20
|
+
name: string;
|
|
21
|
+
reason: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Result of scanning a parent folder for group-launchable repositories.
|
|
25
|
+
*/
|
|
26
|
+
export interface DiscoverResult {
|
|
27
|
+
repos: DiscoveredRepo[];
|
|
28
|
+
skipped: SkippedRepo[];
|
|
29
|
+
/**
|
|
30
|
+
* True when the folder itself is a git repository (a `.git` entry directly
|
|
31
|
+
* under it) — the user probably pointed at a repo instead of its parent.
|
|
32
|
+
*/
|
|
33
|
+
folderIsRepo: boolean;
|
|
34
|
+
}
|
|
35
|
+
/** One repository entry inside a persisted group record. */
|
|
36
|
+
export interface GroupRecordRepo {
|
|
37
|
+
name: string;
|
|
38
|
+
path: string;
|
|
39
|
+
project: string;
|
|
40
|
+
}
|
|
41
|
+
/** A single persisted group-launch record in `.hublaunch/group-launches.json`. */
|
|
42
|
+
export interface GroupRecord {
|
|
43
|
+
groupId: string;
|
|
44
|
+
issueName: string;
|
|
45
|
+
createdAt: string;
|
|
46
|
+
folder: string;
|
|
47
|
+
repos: GroupRecordRepo[];
|
|
48
|
+
}
|
|
49
|
+
/** Zod schema for the on-disk `.hublaunch/group-launches.json` file. */
|
|
50
|
+
export declare const GroupRecordSchema: z.ZodObject<{
|
|
51
|
+
groupId: z.ZodString;
|
|
52
|
+
issueName: z.ZodString;
|
|
53
|
+
createdAt: z.ZodString;
|
|
54
|
+
folder: z.ZodString;
|
|
55
|
+
repos: z.ZodArray<z.ZodObject<{
|
|
56
|
+
name: z.ZodString;
|
|
57
|
+
path: z.ZodString;
|
|
58
|
+
project: z.ZodString;
|
|
59
|
+
}, "strip", z.ZodTypeAny, {
|
|
60
|
+
path: string;
|
|
61
|
+
name: string;
|
|
62
|
+
project: string;
|
|
63
|
+
}, {
|
|
64
|
+
path: string;
|
|
65
|
+
name: string;
|
|
66
|
+
project: string;
|
|
67
|
+
}>, "many">;
|
|
68
|
+
}, "strip", z.ZodTypeAny, {
|
|
69
|
+
createdAt: string;
|
|
70
|
+
groupId: string;
|
|
71
|
+
issueName: string;
|
|
72
|
+
folder: string;
|
|
73
|
+
repos: {
|
|
74
|
+
path: string;
|
|
75
|
+
name: string;
|
|
76
|
+
project: string;
|
|
77
|
+
}[];
|
|
78
|
+
}, {
|
|
79
|
+
createdAt: string;
|
|
80
|
+
groupId: string;
|
|
81
|
+
issueName: string;
|
|
82
|
+
folder: string;
|
|
83
|
+
repos: {
|
|
84
|
+
path: string;
|
|
85
|
+
name: string;
|
|
86
|
+
project: string;
|
|
87
|
+
}[];
|
|
88
|
+
}>;
|
|
89
|
+
export declare const GroupLaunchesFileSchema: z.ZodObject<{
|
|
90
|
+
groups: z.ZodArray<z.ZodObject<{
|
|
91
|
+
groupId: z.ZodString;
|
|
92
|
+
issueName: z.ZodString;
|
|
93
|
+
createdAt: z.ZodString;
|
|
94
|
+
folder: z.ZodString;
|
|
95
|
+
repos: z.ZodArray<z.ZodObject<{
|
|
96
|
+
name: z.ZodString;
|
|
97
|
+
path: z.ZodString;
|
|
98
|
+
project: z.ZodString;
|
|
99
|
+
}, "strip", z.ZodTypeAny, {
|
|
100
|
+
path: string;
|
|
101
|
+
name: string;
|
|
102
|
+
project: string;
|
|
103
|
+
}, {
|
|
104
|
+
path: string;
|
|
105
|
+
name: string;
|
|
106
|
+
project: string;
|
|
107
|
+
}>, "many">;
|
|
108
|
+
}, "strip", z.ZodTypeAny, {
|
|
109
|
+
createdAt: string;
|
|
110
|
+
groupId: string;
|
|
111
|
+
issueName: string;
|
|
112
|
+
folder: string;
|
|
113
|
+
repos: {
|
|
114
|
+
path: string;
|
|
115
|
+
name: string;
|
|
116
|
+
project: string;
|
|
117
|
+
}[];
|
|
118
|
+
}, {
|
|
119
|
+
createdAt: string;
|
|
120
|
+
groupId: string;
|
|
121
|
+
issueName: string;
|
|
122
|
+
folder: string;
|
|
123
|
+
repos: {
|
|
124
|
+
path: string;
|
|
125
|
+
name: string;
|
|
126
|
+
project: string;
|
|
127
|
+
}[];
|
|
128
|
+
}>, "many">;
|
|
129
|
+
}, "strip", z.ZodTypeAny, {
|
|
130
|
+
groups: {
|
|
131
|
+
createdAt: string;
|
|
132
|
+
groupId: string;
|
|
133
|
+
issueName: string;
|
|
134
|
+
folder: string;
|
|
135
|
+
repos: {
|
|
136
|
+
path: string;
|
|
137
|
+
name: string;
|
|
138
|
+
project: string;
|
|
139
|
+
}[];
|
|
140
|
+
}[];
|
|
141
|
+
}, {
|
|
142
|
+
groups: {
|
|
143
|
+
createdAt: string;
|
|
144
|
+
groupId: string;
|
|
145
|
+
issueName: string;
|
|
146
|
+
folder: string;
|
|
147
|
+
repos: {
|
|
148
|
+
path: string;
|
|
149
|
+
name: string;
|
|
150
|
+
project: string;
|
|
151
|
+
}[];
|
|
152
|
+
}[];
|
|
153
|
+
}>;
|
|
154
|
+
export type GroupLaunchesFile = z.infer<typeof GroupLaunchesFileSchema>;
|
|
155
|
+
/** Relative path (from a repo root) of the local group-record file. */
|
|
156
|
+
export declare const GROUP_LAUNCHES_RELATIVE_PATH = ".hublaunch/group-launches.json";
|
|
157
|
+
/**
|
|
158
|
+
* Lowercase an issue name and replace every character outside `[A-Za-z0-9_-]`
|
|
159
|
+
* with a hyphen. Used both for group-id generation and plan-filename matching.
|
|
160
|
+
*/
|
|
161
|
+
export declare function slugifyIssueName(issueName: string): string;
|
|
162
|
+
/**
|
|
163
|
+
* Service for the client-side multi-repo `--folder` fan-out: repository
|
|
164
|
+
* discovery, group-id generation, per-repo plan resolution, and local group
|
|
165
|
+
* record persistence. Deliberately free of network calls so it is trivially
|
|
166
|
+
* unit-testable.
|
|
167
|
+
*/
|
|
168
|
+
export declare class GroupLaunchService {
|
|
169
|
+
/**
|
|
170
|
+
* Discover every immediate subdirectory of `folder` that qualifies as a
|
|
171
|
+
* group-launchable repository — one that contains BOTH a `.git` entry and a
|
|
172
|
+
* `.hublaunch/hublaunch.config.{ts,mjs,js}` file. Discovery does not recurse
|
|
173
|
+
* deeper than one level. Non-qualifying subdirectories are returned in the
|
|
174
|
+
* `skipped` list with a reason ("no .git" / "no .hublaunch config").
|
|
175
|
+
*
|
|
176
|
+
* @param folder - Absolute or relative parent-folder path.
|
|
177
|
+
* @returns The qualifying repos, the skipped entries with reasons, and a
|
|
178
|
+
* `folderIsRepo` flag set when `folder` is itself a git repo.
|
|
179
|
+
*/
|
|
180
|
+
discoverRepos(folder: string): DiscoverResult;
|
|
181
|
+
/**
|
|
182
|
+
* Generate a group id of the form `<slug>-<6 hex chars>` where `<slug>` is the
|
|
183
|
+
* slugified issue name, truncated so the full id is ≤ 64 characters. The hex
|
|
184
|
+
* suffix comes from `crypto.randomBytes(3)`.
|
|
185
|
+
*
|
|
186
|
+
* @param issueName - The feature/issue name.
|
|
187
|
+
* @returns A group id matching `^[A-Za-z0-9_-]{1,64}$`.
|
|
188
|
+
*/
|
|
189
|
+
generateGroupId(issueName: string): string;
|
|
190
|
+
/**
|
|
191
|
+
* Resolve the plan file to launch for a repository.
|
|
192
|
+
*
|
|
193
|
+
* Candidates are `*.md` files under the repo's configured plans directory
|
|
194
|
+
* (`repoConfig.planPath`), searched recursively. Preference order: the newest
|
|
195
|
+
* file (by `YYYY-MM-DD-HH:MM` filename prefix, falling back to mtime) whose
|
|
196
|
+
* filename contains the issue-name slug; if none match, the newest plan file
|
|
197
|
+
* overall.
|
|
198
|
+
*
|
|
199
|
+
* @param repo - The discovered repository.
|
|
200
|
+
* @param repoConfig - That repo's loaded config (absolute `planPath`).
|
|
201
|
+
* @param issueName - The feature/issue name whose slug is matched.
|
|
202
|
+
* @returns The repo-relative plan path (e.g. `.hublaunch/plans/…md`), or null
|
|
203
|
+
* when the repo has no plan files at all.
|
|
204
|
+
*/
|
|
205
|
+
resolvePlan(repo: DiscoveredRepo, repoConfig: Config, issueName: string): string | null;
|
|
206
|
+
/**
|
|
207
|
+
* Append a group record to `.hublaunch/group-launches.json` in each member
|
|
208
|
+
* repo. Best-effort: a corrupt existing file is treated as empty and
|
|
209
|
+
* rewritten fresh; any write failure warns and is otherwise ignored (it must
|
|
210
|
+
* never fail the launch).
|
|
211
|
+
*
|
|
212
|
+
* @param repos - Repos whose local record file should receive the record.
|
|
213
|
+
* @param record - The group record to append.
|
|
214
|
+
*/
|
|
215
|
+
recordGroup(repos: DiscoveredRepo[], record: GroupRecord): void;
|
|
216
|
+
/**
|
|
217
|
+
* Read the most recently recorded group id from the current repo's
|
|
218
|
+
* `.hublaunch/group-launches.json`. Used by a bare `--show-group` (no id).
|
|
219
|
+
*
|
|
220
|
+
* @param repoRoot - The repository root to read the record file from.
|
|
221
|
+
* @returns The latest group's id, or null when the file is absent/empty/corrupt.
|
|
222
|
+
*/
|
|
223
|
+
latestRecordedGroupId(repoRoot: string): string | null;
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Validate a user-supplied group id against the server's format rule. Returns
|
|
227
|
+
* the server's exact error wording on failure, or null when valid.
|
|
228
|
+
*/
|
|
229
|
+
export declare function validateGroupId(groupId: string): string | null;
|
|
230
|
+
//# sourceMappingURL=GroupLaunchService.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"GroupLaunchService.d.ts","sourceRoot":"","sources":["../../../src/services/group/GroupLaunchService.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAgBnD,qEAAqE;AACrE,eAAO,MAAM,cAAc,QAA0B,CAAC;AAEtD;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC7B,gFAAgF;IAChF,IAAI,EAAE,MAAM,CAAC;IACb,4CAA4C;IAC5C,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;GAGG;AACH,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,cAAc,EAAE,CAAC;IACxB,OAAO,EAAE,WAAW,EAAE,CAAC;IACvB;;;OAGG;IACH,YAAY,EAAE,OAAO,CAAC;CACvB;AAED,4DAA4D;AAC5D,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,kFAAkF;AAClF,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,eAAe,EAAE,CAAC;CAC1B;AAED,wEAAwE;AACxE,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAY5B,CAAC;AAEH,eAAO,MAAM,uBAAuB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAElC,CAAC;AAEH,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAC;AAExE,uEAAuE;AACvE,eAAO,MAAM,4BAA4B,mCAAmC,CAAC;AAc7E;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAE1D;AAmED;;;;;GAKG;AACH,qBAAa,kBAAkB;IAC7B;;;;;;;;;;OAUG;IACH,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,cAAc;IAmC7C;;;;;;;OAOG;IACH,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM;IAU1C;;;;;;;;;;;;;;OAcG;IACH,WAAW,CACT,IAAI,EAAE,cAAc,EACpB,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,GAChB,MAAM,GAAG,IAAI;IAiBhB;;;;;;;;OAQG;IACH,WAAW,CAAC,KAAK,EAAE,cAAc,EAAE,EAAE,MAAM,EAAE,WAAW,GAAG,IAAI;IAgC/D;;;;;;OAMG;IACH,qBAAqB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;CAcvD;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAK9D"}
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
import { existsSync, readdirSync, readFileSync, statSync, writeFileSync, } from 'fs';
|
|
3
|
+
import { join, relative } from 'path';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { logger } from '../../utils/logger.js';
|
|
6
|
+
/**
|
|
7
|
+
* The three config-file names a repository may use, in preference order. Mirrors
|
|
8
|
+
* the triple-extension check in `src/index.ts` and `ConfigLoader`.
|
|
9
|
+
*/
|
|
10
|
+
const CONFIG_FILE_NAMES = [
|
|
11
|
+
'hublaunch.config.ts',
|
|
12
|
+
'hublaunch.config.mjs',
|
|
13
|
+
'hublaunch.config.js',
|
|
14
|
+
];
|
|
15
|
+
/** Maximum length of a generated/accepted group id (server-enforced). */
|
|
16
|
+
const MAX_GROUP_ID_LENGTH = 64;
|
|
17
|
+
/** The server's `groupId` validation regex (hula-server PR #505). */
|
|
18
|
+
export const GROUP_ID_REGEX = /^[A-Za-z0-9_-]{1,64}$/;
|
|
19
|
+
/** Zod schema for the on-disk `.hublaunch/group-launches.json` file. */
|
|
20
|
+
export const GroupRecordSchema = z.object({
|
|
21
|
+
groupId: z.string(),
|
|
22
|
+
issueName: z.string(),
|
|
23
|
+
createdAt: z.string(),
|
|
24
|
+
folder: z.string(),
|
|
25
|
+
repos: z.array(z.object({
|
|
26
|
+
name: z.string(),
|
|
27
|
+
path: z.string(),
|
|
28
|
+
project: z.string(),
|
|
29
|
+
})),
|
|
30
|
+
});
|
|
31
|
+
export const GroupLaunchesFileSchema = z.object({
|
|
32
|
+
groups: z.array(GroupRecordSchema),
|
|
33
|
+
});
|
|
34
|
+
/** Relative path (from a repo root) of the local group-record file. */
|
|
35
|
+
export const GROUP_LAUNCHES_RELATIVE_PATH = '.hublaunch/group-launches.json';
|
|
36
|
+
/**
|
|
37
|
+
* Return the first existing `.hublaunch/hublaunch.config.{ts,mjs,js}` path in a
|
|
38
|
+
* directory, or null when none exists.
|
|
39
|
+
*/
|
|
40
|
+
function findConfigFile(dir) {
|
|
41
|
+
for (const name of CONFIG_FILE_NAMES) {
|
|
42
|
+
const p = join(dir, '.hublaunch', name);
|
|
43
|
+
if (existsSync(p))
|
|
44
|
+
return p;
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Lowercase an issue name and replace every character outside `[A-Za-z0-9_-]`
|
|
50
|
+
* with a hyphen. Used both for group-id generation and plan-filename matching.
|
|
51
|
+
*/
|
|
52
|
+
export function slugifyIssueName(issueName) {
|
|
53
|
+
return issueName.toLowerCase().replace(/[^a-z0-9_-]/g, '-');
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Extract the sortable `YYYY-MM-DD-HH:MM` timestamp prefix from a plan filename.
|
|
57
|
+
* Returns the prefix string (lexicographically sortable) or null when absent.
|
|
58
|
+
*/
|
|
59
|
+
function timestampPrefix(filename) {
|
|
60
|
+
const m = filename.match(/^(\d{4}-\d{2}-\d{2}-\d{2}:\d{2})/);
|
|
61
|
+
return m ? m[1] : null;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Recursively collect every `*.md` file under `dir`.
|
|
65
|
+
*/
|
|
66
|
+
function collectPlanFiles(dir) {
|
|
67
|
+
const out = [];
|
|
68
|
+
let entries;
|
|
69
|
+
try {
|
|
70
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
for (const entry of entries) {
|
|
76
|
+
const full = join(dir, entry.name);
|
|
77
|
+
if (entry.isDirectory()) {
|
|
78
|
+
out.push(...collectPlanFiles(full));
|
|
79
|
+
}
|
|
80
|
+
else if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
81
|
+
let mtimeMs = 0;
|
|
82
|
+
try {
|
|
83
|
+
mtimeMs = statSync(full).mtimeMs;
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
mtimeMs = 0;
|
|
87
|
+
}
|
|
88
|
+
out.push({
|
|
89
|
+
absPath: full,
|
|
90
|
+
filename: entry.name,
|
|
91
|
+
prefix: timestampPrefix(entry.name),
|
|
92
|
+
mtimeMs,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return out;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Order two plan candidates newest-first: prefer the larger `YYYY-MM-DD-HH:MM`
|
|
100
|
+
* filename prefix; candidates without a prefix sort after those with one; ties
|
|
101
|
+
* fall back to mtime (newest first).
|
|
102
|
+
*/
|
|
103
|
+
function comparePlansNewestFirst(a, b) {
|
|
104
|
+
if (a.prefix && b.prefix) {
|
|
105
|
+
if (a.prefix !== b.prefix)
|
|
106
|
+
return a.prefix > b.prefix ? -1 : 1;
|
|
107
|
+
return b.mtimeMs - a.mtimeMs;
|
|
108
|
+
}
|
|
109
|
+
if (a.prefix && !b.prefix)
|
|
110
|
+
return -1;
|
|
111
|
+
if (!a.prefix && b.prefix)
|
|
112
|
+
return 1;
|
|
113
|
+
return b.mtimeMs - a.mtimeMs;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Service for the client-side multi-repo `--folder` fan-out: repository
|
|
117
|
+
* discovery, group-id generation, per-repo plan resolution, and local group
|
|
118
|
+
* record persistence. Deliberately free of network calls so it is trivially
|
|
119
|
+
* unit-testable.
|
|
120
|
+
*/
|
|
121
|
+
export class GroupLaunchService {
|
|
122
|
+
/**
|
|
123
|
+
* Discover every immediate subdirectory of `folder` that qualifies as a
|
|
124
|
+
* group-launchable repository — one that contains BOTH a `.git` entry and a
|
|
125
|
+
* `.hublaunch/hublaunch.config.{ts,mjs,js}` file. Discovery does not recurse
|
|
126
|
+
* deeper than one level. Non-qualifying subdirectories are returned in the
|
|
127
|
+
* `skipped` list with a reason ("no .git" / "no .hublaunch config").
|
|
128
|
+
*
|
|
129
|
+
* @param folder - Absolute or relative parent-folder path.
|
|
130
|
+
* @returns The qualifying repos, the skipped entries with reasons, and a
|
|
131
|
+
* `folderIsRepo` flag set when `folder` is itself a git repo.
|
|
132
|
+
*/
|
|
133
|
+
discoverRepos(folder) {
|
|
134
|
+
const repos = [];
|
|
135
|
+
const skipped = [];
|
|
136
|
+
const folderIsRepo = existsSync(join(folder, '.git'));
|
|
137
|
+
let entries;
|
|
138
|
+
try {
|
|
139
|
+
entries = readdirSync(folder, { withFileTypes: true });
|
|
140
|
+
}
|
|
141
|
+
catch (err) {
|
|
142
|
+
throw new Error(`Cannot read folder "${folder}": ${err instanceof Error ? err.message : String(err)}`);
|
|
143
|
+
}
|
|
144
|
+
for (const entry of entries) {
|
|
145
|
+
if (!entry.isDirectory())
|
|
146
|
+
continue;
|
|
147
|
+
const dir = join(folder, entry.name);
|
|
148
|
+
const hasGit = existsSync(join(dir, '.git'));
|
|
149
|
+
const hasConfig = findConfigFile(dir) !== null;
|
|
150
|
+
if (hasGit && hasConfig) {
|
|
151
|
+
repos.push({ name: entry.name, path: dir });
|
|
152
|
+
}
|
|
153
|
+
else if (!hasGit) {
|
|
154
|
+
skipped.push({ name: entry.name, reason: 'no .git' });
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
skipped.push({ name: entry.name, reason: 'no .hublaunch config' });
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
// Stable, deterministic discovery order (directory name).
|
|
161
|
+
repos.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
162
|
+
return { repos, skipped, folderIsRepo };
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Generate a group id of the form `<slug>-<6 hex chars>` where `<slug>` is the
|
|
166
|
+
* slugified issue name, truncated so the full id is ≤ 64 characters. The hex
|
|
167
|
+
* suffix comes from `crypto.randomBytes(3)`.
|
|
168
|
+
*
|
|
169
|
+
* @param issueName - The feature/issue name.
|
|
170
|
+
* @returns A group id matching `^[A-Za-z0-9_-]{1,64}$`.
|
|
171
|
+
*/
|
|
172
|
+
generateGroupId(issueName) {
|
|
173
|
+
const hex = randomBytes(3).toString('hex'); // 6 hex chars
|
|
174
|
+
const suffix = `-${hex}`;
|
|
175
|
+
const maxSlug = MAX_GROUP_ID_LENGTH - suffix.length;
|
|
176
|
+
let slug = slugifyIssueName(issueName).slice(0, Math.max(0, maxSlug));
|
|
177
|
+
// Avoid a leading/trailing bare hyphen pile-up while staying within regex.
|
|
178
|
+
if (slug === '')
|
|
179
|
+
slug = 'group';
|
|
180
|
+
return `${slug}${suffix}`;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Resolve the plan file to launch for a repository.
|
|
184
|
+
*
|
|
185
|
+
* Candidates are `*.md` files under the repo's configured plans directory
|
|
186
|
+
* (`repoConfig.planPath`), searched recursively. Preference order: the newest
|
|
187
|
+
* file (by `YYYY-MM-DD-HH:MM` filename prefix, falling back to mtime) whose
|
|
188
|
+
* filename contains the issue-name slug; if none match, the newest plan file
|
|
189
|
+
* overall.
|
|
190
|
+
*
|
|
191
|
+
* @param repo - The discovered repository.
|
|
192
|
+
* @param repoConfig - That repo's loaded config (absolute `planPath`).
|
|
193
|
+
* @param issueName - The feature/issue name whose slug is matched.
|
|
194
|
+
* @returns The repo-relative plan path (e.g. `.hublaunch/plans/…md`), or null
|
|
195
|
+
* when the repo has no plan files at all.
|
|
196
|
+
*/
|
|
197
|
+
resolvePlan(repo, repoConfig, issueName) {
|
|
198
|
+
const planDir = repoConfig.planPath;
|
|
199
|
+
const candidates = collectPlanFiles(planDir);
|
|
200
|
+
if (candidates.length === 0)
|
|
201
|
+
return null;
|
|
202
|
+
const slug = slugifyIssueName(issueName);
|
|
203
|
+
const matching = candidates.filter((c) => c.filename.toLowerCase().includes(slug));
|
|
204
|
+
const pool = matching.length > 0 ? matching : candidates;
|
|
205
|
+
pool.sort(comparePlansNewestFirst);
|
|
206
|
+
const chosen = pool[0];
|
|
207
|
+
// Return a repo-relative path for display and per-repo launching.
|
|
208
|
+
return relative(repo.path, chosen.absPath);
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Append a group record to `.hublaunch/group-launches.json` in each member
|
|
212
|
+
* repo. Best-effort: a corrupt existing file is treated as empty and
|
|
213
|
+
* rewritten fresh; any write failure warns and is otherwise ignored (it must
|
|
214
|
+
* never fail the launch).
|
|
215
|
+
*
|
|
216
|
+
* @param repos - Repos whose local record file should receive the record.
|
|
217
|
+
* @param record - The group record to append.
|
|
218
|
+
*/
|
|
219
|
+
recordGroup(repos, record) {
|
|
220
|
+
for (const repo of repos) {
|
|
221
|
+
const filePath = join(repo.path, GROUP_LAUNCHES_RELATIVE_PATH);
|
|
222
|
+
try {
|
|
223
|
+
let file = { groups: [] };
|
|
224
|
+
if (existsSync(filePath)) {
|
|
225
|
+
try {
|
|
226
|
+
const parsed = JSON.parse(readFileSync(filePath, 'utf-8'));
|
|
227
|
+
const validated = GroupLaunchesFileSchema.safeParse(parsed);
|
|
228
|
+
file = validated.success ? validated.data : { groups: [] };
|
|
229
|
+
if (!validated.success) {
|
|
230
|
+
logger.warning(`Group record at ${filePath} was unreadable — rewriting fresh.`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
logger.warning(`Group record at ${filePath} was corrupt — rewriting fresh.`);
|
|
235
|
+
file = { groups: [] };
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
file.groups.push(record);
|
|
239
|
+
writeFileSync(filePath, JSON.stringify(file, null, 2) + '\n', 'utf-8');
|
|
240
|
+
}
|
|
241
|
+
catch (err) {
|
|
242
|
+
logger.warning(`Could not write group record to ${filePath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Read the most recently recorded group id from the current repo's
|
|
248
|
+
* `.hublaunch/group-launches.json`. Used by a bare `--show-group` (no id).
|
|
249
|
+
*
|
|
250
|
+
* @param repoRoot - The repository root to read the record file from.
|
|
251
|
+
* @returns The latest group's id, or null when the file is absent/empty/corrupt.
|
|
252
|
+
*/
|
|
253
|
+
latestRecordedGroupId(repoRoot) {
|
|
254
|
+
const filePath = join(repoRoot, GROUP_LAUNCHES_RELATIVE_PATH);
|
|
255
|
+
if (!existsSync(filePath))
|
|
256
|
+
return null;
|
|
257
|
+
try {
|
|
258
|
+
const parsed = JSON.parse(readFileSync(filePath, 'utf-8'));
|
|
259
|
+
const validated = GroupLaunchesFileSchema.safeParse(parsed);
|
|
260
|
+
if (!validated.success)
|
|
261
|
+
return null;
|
|
262
|
+
const groups = validated.data.groups;
|
|
263
|
+
if (groups.length === 0)
|
|
264
|
+
return null;
|
|
265
|
+
return groups[groups.length - 1].groupId;
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Validate a user-supplied group id against the server's format rule. Returns
|
|
274
|
+
* the server's exact error wording on failure, or null when valid.
|
|
275
|
+
*/
|
|
276
|
+
export function validateGroupId(groupId) {
|
|
277
|
+
if (!GROUP_ID_REGEX.test(groupId)) {
|
|
278
|
+
return 'Invalid groupId format. Expected 1-64 characters of letters, digits, hyphen, underscore.';
|
|
279
|
+
}
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
//# sourceMappingURL=GroupLaunchService.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"GroupLaunchService.js","sourceRoot":"","sources":["../../../src/services/group/GroupLaunchService.ts"],"names":[],"mappings":"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"}
|
|
@@ -35,7 +35,7 @@ hula --version 2>/dev/null
|
|
|
35
35
|
```
|
|
36
36
|
|
|
37
37
|
If initialized, `Read .hublaunch/hublaunch.config.js` and note (never print the values themselves, only presence/absence):
|
|
38
|
-
- Is `hulaApiKey` (or `anthropicApiKey
|
|
38
|
+
- Is `hulaApiKey` (or `anthropicApiKey`) present with a non-empty value? → user has likely completed `hula login`.
|
|
39
39
|
- If `.hublaunch/trackedPlans.json` exists, `Read` it — does it have any entries? → user has launched at least one plan before.
|
|
40
40
|
|
|
41
41
|
If any check fails (not a git repo, config unreadable), treat that signal as unknown and continue — don't block on it.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: hula-launch
|
|
3
3
|
description: "Launch a validated plan: creates the GitHub issue and starts the cloud run that ends in a verified PR (offered automatically after /hula-plan). Use when the user asks to launch a plan."
|
|
4
4
|
disable-model-invocation: true
|
|
5
|
-
argument-hint: <branch-name> [<plan-path>] [--handoff <username>] [--test] [--skip-regression] [--kill] [--kill-and-relaunch]
|
|
5
|
+
argument-hint: <branch-name> [<plan-path>] [--handoff <username>] [--test] [--skip-regression] [--kill] [--kill-and-relaunch] [--folder <path>] [--group-id <id>] [--plan <repo>=<path>]
|
|
6
6
|
allowed-tools: Bash Read
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -18,6 +18,11 @@ From `$ARGUMENTS`:
|
|
|
18
18
|
- **--skip-regression** (optional): the bare flag `--skip-regression`, anywhere in `$ARGUMENTS`. Force-skips the regression-tests pipeline step for this one launch (overrides any `steps.regression.skip` config default).
|
|
19
19
|
- **--kill** (optional): the bare flag `--kill`, anywhere in `$ARGUMENTS`. Stops the in-flight task for this branch name **without** relaunching. A plan path is **not** required with `--kill` — the branch name alone is enough.
|
|
20
20
|
- **--kill-and-relaunch** (optional): the bare flag `--kill-and-relaunch`, anywhere in `$ARGUMENTS`. Cancels the in-flight task, resets stale state, and launches a fresh task (needs a plan path, like a normal launch).
|
|
21
|
+
- **--folder** (optional): `--folder <path>`. Multi-repo mode — launches the feature in **every initialized repo directly under `<path>`** as one correlated feature group.
|
|
22
|
+
- ⚠️ **This `--folder` is DIFFERENT from `/hula-plan`'s `--folder`.** Here `<path>` is the **PARENT directory that contains sibling repos** (e.g. `~/code/myapp`, which holds `~/code/myapp/mobile` and `~/code/myapp/api`). In `/hula-plan`, `--folder <subfolder>` names a **plans subdirectory**. Never conflate the two.
|
|
23
|
+
- In folder mode the plan path is resolved **per repository** — do NOT pass a positional plan path. A single positional is the branch name.
|
|
24
|
+
- **--group-id** (optional): `--group-id <id>`. Use an explicit group id instead of generating one (join/retry an existing group). Valid ids match `^[A-Za-z0-9_-]{1,64}$`.
|
|
25
|
+
- **--plan** (optional, repeatable): `--plan <repo>=<path>`. Override the auto-resolved plan for one repo in `--folder` mode. `<repo>` is the discovered directory name; `<path>` is relative to that repo's root.
|
|
21
26
|
|
|
22
27
|
`--kill` and `--kill-and-relaunch` are mutually exclusive; if the user passes both, stop with:
|
|
23
28
|
```
|
|
@@ -83,6 +88,22 @@ hula script launch-run -- <branch-name> --kill
|
|
|
83
88
|
hula script launch-run -- <plan-path> <branch-name> --kill-and-relaunch
|
|
84
89
|
```
|
|
85
90
|
|
|
91
|
+
**Launch a multi-repo feature group (`--folder`)** — pass only the branch name (no plan path); plans resolve per repo:
|
|
92
|
+
```bash
|
|
93
|
+
hula script launch-run -- <branch-name> --folder <parent-path>
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
**Override a repo's plan, or join/retry an existing group:**
|
|
97
|
+
```bash
|
|
98
|
+
hula script launch-run -- <branch-name> --folder <parent-path> --plan api=.hublaunch/plans/2026-08-10-11:00-login-api.md
|
|
99
|
+
hula script launch-run -- <branch-name> --folder <parent-path> --group-id login-a3f9c1
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
**Stop every member's in-flight task (`--folder --kill`):**
|
|
103
|
+
```bash
|
|
104
|
+
hula script launch-run -- <branch-name> --folder <parent-path> --kill
|
|
105
|
+
```
|
|
106
|
+
|
|
86
107
|
## Step 3: Show the result
|
|
87
108
|
|
|
88
109
|
Parse the JSON output from the script:
|
|
@@ -97,6 +118,22 @@ Parse the JSON output from the script:
|
|
|
97
118
|
|
|
98
119
|
The `cliOutput` already says whether a task was cancelled (`Cancelled '<branch>'`) or there was nothing to cancel (`No active task to cancel for '<branch>'`). Do **not** emit a `hula-issue` comment for a pure kill.
|
|
99
120
|
|
|
121
|
+
- If `status` is `"success"` **and this was a `--folder` (feature-group) run** — indicated by a non-empty `folder` and a `groupId` — there is no single issue/PR. Display a group summary instead of the single-launch template:
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
✅ Launched feature group `<groupId>`
|
|
125
|
+
|
|
126
|
+
<cliOutput>
|
|
127
|
+
|
|
128
|
+
<!-- hula-group: <groupId> -->
|
|
129
|
+
|
|
130
|
+
**Next Steps:**
|
|
131
|
+
- Use `/hula-launch <branch-name> --show-group <groupId>` (or `hula launch --show-group <groupId>`) to see combined per-repo status
|
|
132
|
+
- A failing member does not stop the others — the summary above lists ✓/✗ per repo and an exact retry command
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
The `cliOutput` already contains the per-repo roster, ✓/✗ summary, and any retry commands. Do **not** emit a `hula-issue` comment for a group launch (issue numbers are per-repo).
|
|
136
|
+
|
|
100
137
|
- Otherwise (`status` is `"success"` with a non-empty `issueNumber` — a normal launch or `--kill-and-relaunch`), display:
|
|
101
138
|
|
|
102
139
|
```
|
|
@@ -257,9 +257,9 @@ If an identifier is empty, fall back to showing the `cliOutput` field.
|
|
|
257
257
|
|
|
258
258
|
## Important Notes
|
|
259
259
|
|
|
260
|
-
- Do NOT pre-check credentials. The CLI resolves the Anthropic OAuth token
|
|
261
|
-
|
|
262
|
-
|
|
260
|
+
- Do NOT pre-check credentials. The CLI resolves the Anthropic OAuth token and
|
|
261
|
+
GitHub token from flags/config/env and reports a clear error if any are
|
|
262
|
+
missing; the wrappers surface it.
|
|
263
263
|
- Do NOT echo secrets.
|
|
264
264
|
- Generated action files live under `.hublaunch/skills/` and are committed to
|
|
265
265
|
`origin/main` via a temporary worktree — never on the user's current branch.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure/sync helpers for editing the `.hublaunch/hublaunch.config.js` source
|
|
3
|
+
* file as text. Kept side-effect free (no filesystem access) so the string
|
|
4
|
+
* transformations are unit-testable in isolation — same rationale as
|
|
5
|
+
* `appendGitignoreEntry` in `src/commands/init.ts`. Callers own reading and
|
|
6
|
+
* writing the file.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Insert or replace the `anthropicApiKey` line in a config file's source text.
|
|
10
|
+
*
|
|
11
|
+
* Mirrors the comment-out-then-insert approach used by `saveApiKeyToConfig` in
|
|
12
|
+
* `src/commands/login.ts` for `hulaApiKey`:
|
|
13
|
+
* - Any existing `anthropicApiKey:` line — whether active or commented out
|
|
14
|
+
* (including the placeholder init writes) — is commented out.
|
|
15
|
+
* - The new active line is inserted immediately after the config opening
|
|
16
|
+
* (`export const config = {` or `export default {`).
|
|
17
|
+
*
|
|
18
|
+
* If no config opening is found the content is returned unchanged (the caller
|
|
19
|
+
* degrades gracefully rather than corrupting the file). Never echoes the key
|
|
20
|
+
* anywhere but the returned string.
|
|
21
|
+
*/
|
|
22
|
+
export declare function upsertAnthropicApiKey(content: string, key: string): string;
|
|
23
|
+
//# sourceMappingURL=config-file-edit.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config-file-edit.d.ts","sourceRoot":"","sources":["../../src/utils/config-file-edit.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH;;;;;;;;;;;;;GAaG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAuB1E"}
|