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.
Files changed (62) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +69 -14
  3. package/dist/commands/init.d.ts +21 -2
  4. package/dist/commands/init.d.ts.map +1 -1
  5. package/dist/commands/init.js +121 -38
  6. package/dist/commands/init.js.map +1 -1
  7. package/dist/commands/launch.d.ts +91 -0
  8. package/dist/commands/launch.d.ts.map +1 -1
  9. package/dist/commands/launch.js +885 -144
  10. package/dist/commands/launch.js.map +1 -1
  11. package/dist/commands/login.d.ts +38 -0
  12. package/dist/commands/login.d.ts.map +1 -1
  13. package/dist/commands/login.js +107 -11
  14. package/dist/commands/login.js.map +1 -1
  15. package/dist/commands/upload.d.ts +9 -2
  16. package/dist/commands/upload.d.ts.map +1 -1
  17. package/dist/commands/upload.js +16 -6
  18. package/dist/commands/upload.js.map +1 -1
  19. package/dist/scripts/launch-run.d.ts +6 -0
  20. package/dist/scripts/launch-run.d.ts.map +1 -1
  21. package/dist/scripts/launch-run.js +70 -9
  22. package/dist/scripts/launch-run.js.map +1 -1
  23. package/dist/services/api/HulaApiClient.d.ts +53 -0
  24. package/dist/services/api/HulaApiClient.d.ts.map +1 -1
  25. package/dist/services/api/HulaApiClient.js +18 -0
  26. package/dist/services/api/HulaApiClient.js.map +1 -1
  27. package/dist/services/git/FilePublishService.d.ts +9 -1
  28. package/dist/services/git/FilePublishService.d.ts.map +1 -1
  29. package/dist/services/git/FilePublishService.js +13 -3
  30. package/dist/services/git/FilePublishService.js.map +1 -1
  31. package/dist/services/git/GitService.d.ts +19 -0
  32. package/dist/services/git/GitService.d.ts.map +1 -1
  33. package/dist/services/git/GitService.js +56 -31
  34. package/dist/services/git/GitService.js.map +1 -1
  35. package/dist/services/git/WorktreeService.d.ts +9 -0
  36. package/dist/services/git/WorktreeService.d.ts.map +1 -1
  37. package/dist/services/git/WorktreeService.js +6 -3
  38. package/dist/services/git/WorktreeService.js.map +1 -1
  39. package/dist/services/group/GroupLaunchService.d.ts +230 -0
  40. package/dist/services/group/GroupLaunchService.d.ts.map +1 -0
  41. package/dist/services/group/GroupLaunchService.js +282 -0
  42. package/dist/services/group/GroupLaunchService.js.map +1 -0
  43. package/dist/templates/skills/hula-help/SKILL.md +1 -1
  44. package/dist/templates/skills/hula-launch/SKILL.md +38 -1
  45. package/dist/templates/skills/hula-schedule/SKILL.md +3 -3
  46. package/dist/utils/config-file-edit.d.ts +23 -0
  47. package/dist/utils/config-file-edit.d.ts.map +1 -0
  48. package/dist/utils/config-file-edit.js +37 -0
  49. package/dist/utils/config-file-edit.js.map +1 -0
  50. package/dist/utils/ephemeral-credentials.d.ts +8 -0
  51. package/dist/utils/ephemeral-credentials.d.ts.map +1 -1
  52. package/dist/utils/ephemeral-credentials.js +2 -1
  53. package/dist/utils/ephemeral-credentials.js.map +1 -1
  54. package/dist/utils/github-cli.d.ts +1 -1
  55. package/dist/utils/github-cli.d.ts.map +1 -1
  56. package/dist/utils/github-cli.js +15 -7
  57. package/dist/utils/github-cli.js.map +1 -1
  58. package/dist/utils/project-resolver.d.ts +1 -1
  59. package/dist/utils/project-resolver.d.ts.map +1 -1
  60. package/dist/utils/project-resolver.js +2 -2
  61. package/dist/utils/project-resolver.js.map +1 -1
  62. 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`/`daytonaApiKey`) present with a non-empty value? → user has likely completed `hula login`.
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
- Daytona key, and GitHub token from flags/config/env and reports a clear error
262
- if any are missing; the wrappers surface it.
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"}