hub-launch 1.25.0 â 1.27.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 +24 -0
- package/dist/commands/approve.d.ts.map +1 -1
- package/dist/commands/approve.js +15 -0
- package/dist/commands/approve.js.map +1 -1
- package/dist/commands/init.d.ts +12 -2
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +239 -216
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/launch.d.ts +15 -0
- package/dist/commands/launch.d.ts.map +1 -1
- package/dist/commands/launch.js +73 -0
- package/dist/commands/launch.js.map +1 -1
- package/dist/commands/preview.js +4 -16
- package/dist/commands/preview.js.map +1 -1
- package/dist/services/group/GroupLaunchService.d.ts +6 -6
- package/dist/services/hooks/HookExecutor.d.ts +24 -0
- package/dist/services/hooks/HookExecutor.d.ts.map +1 -1
- package/dist/services/hooks/HookExecutor.js +61 -3
- package/dist/services/hooks/HookExecutor.js.map +1 -1
- package/dist/types/config.schema.d.ts +8 -8
- package/dist/types/config.schema.js +1 -1
- package/dist/types/config.schema.js.map +1 -1
- package/package.json +9 -1
package/dist/commands/init.js
CHANGED
|
@@ -129,200 +129,62 @@ export function initCommand(program, _config) {
|
|
|
129
129
|
}
|
|
130
130
|
});
|
|
131
131
|
}
|
|
132
|
-
|
|
133
|
-
* Get the deployment startup script template based on auth provider
|
|
134
|
-
*/
|
|
135
|
-
function getDeploymentStartupTemplate(authProvider) {
|
|
136
|
-
const header = `#!/usr/bin/env tsx
|
|
137
|
-
/**
|
|
138
|
-
* Deployment Startup Script
|
|
139
|
-
* Automatically logs in to preview deployment before opening
|
|
140
|
-
*
|
|
141
|
-
* Context passed as first argument (JSON string):
|
|
142
|
-
* {
|
|
143
|
-
* deploymentUrl: string;
|
|
144
|
-
* issueNumber?: number;
|
|
145
|
-
* prNumber?: number;
|
|
146
|
-
* prTitle?: string;
|
|
147
|
-
* prUrl?: string;
|
|
148
|
-
* }
|
|
149
|
-
*/
|
|
150
|
-
|
|
151
|
-
import { chromium } from "@playwright/test";
|
|
152
|
-
|
|
153
|
-
interface HookContext {
|
|
154
|
-
deploymentUrl: string;
|
|
155
|
-
issueNumber?: number;
|
|
156
|
-
prNumber?: number;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
async function main() {
|
|
160
|
-
const contextJson = process.argv[2];
|
|
161
|
-
if (!contextJson) {
|
|
162
|
-
console.error("Error: No context provided");
|
|
163
|
-
process.exit(1);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
const context: HookContext = JSON.parse(contextJson);
|
|
167
|
-
console.log(\`đ Starting preview for: \${context.deploymentUrl}\`);
|
|
168
|
-
|
|
169
|
-
// Browser headless mode: Use env var or default to visible
|
|
170
|
-
const headless = process.env.CI === "true" || process.env.HEADLESS === "true";
|
|
171
|
-
|
|
172
|
-
const browser = await chromium.launch({
|
|
173
|
-
headless,
|
|
174
|
-
slowMo: headless ? 0 : 100,
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
const page = await browser.newPage();
|
|
178
|
-
|
|
179
|
-
try {
|
|
180
|
-
await page.goto(context.deploymentUrl);
|
|
181
|
-
`;
|
|
182
|
-
if (authProvider === 'clerk') {
|
|
183
|
-
return (header +
|
|
184
|
-
`
|
|
185
|
-
// Wait for Clerk authentication widget
|
|
186
|
-
await page.waitForSelector('[data-clerk-id]', { timeout: 5000 });
|
|
187
|
-
|
|
188
|
-
// Click sign-in button
|
|
189
|
-
await page.click('button:has-text("Sign in")');
|
|
190
|
-
|
|
191
|
-
// Fill in credentials from environment
|
|
192
|
-
const email = process.env.TEST_USER_EMAIL;
|
|
193
|
-
const password = process.env.TEST_USER_PASSWORD;
|
|
194
|
-
|
|
195
|
-
if (!email || !password) {
|
|
196
|
-
throw new Error(
|
|
197
|
-
"Required: TEST_USER_EMAIL and TEST_USER_PASSWORD environment variables"
|
|
198
|
-
);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// Enter email
|
|
202
|
-
await page.fill('input[name="identifier"]', email);
|
|
203
|
-
await page.click('button:has-text("Continue")');
|
|
204
|
-
|
|
205
|
-
// Enter password
|
|
206
|
-
await page.fill('input[name="password"]', password);
|
|
207
|
-
await page.click('button:has-text("Continue")');
|
|
208
|
-
|
|
209
|
-
// Wait for successful login
|
|
210
|
-
await page.waitForURL(\`\${context.deploymentUrl}/**\`, {
|
|
211
|
-
timeout: 10000
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
console.log("â
Successfully logged in with Clerk!");
|
|
215
|
-
console.log("Browser will remain open for testing.");
|
|
216
|
-
|
|
217
|
-
// Keep browser open - user closes when done
|
|
218
|
-
// Don't call browser.close()
|
|
219
|
-
|
|
220
|
-
} catch (error) {
|
|
221
|
-
console.error("â Login failed:", error);
|
|
222
|
-
await browser.close();
|
|
223
|
-
process.exit(1);
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
main().catch(console.error);
|
|
228
|
-
`);
|
|
229
|
-
}
|
|
230
|
-
if (authProvider === 'auth0') {
|
|
231
|
-
return (header +
|
|
232
|
-
`
|
|
233
|
-
// Wait for Auth0 login form
|
|
234
|
-
await page.waitForSelector('[name="username"]', { timeout: 5000 });
|
|
235
|
-
|
|
236
|
-
// Fill in credentials from environment
|
|
237
|
-
const email = process.env.TEST_USER_EMAIL;
|
|
238
|
-
const password = process.env.TEST_USER_PASSWORD;
|
|
239
|
-
|
|
240
|
-
if (!email || !password) {
|
|
241
|
-
throw new Error(
|
|
242
|
-
"Required: TEST_USER_EMAIL and TEST_USER_PASSWORD environment variables"
|
|
243
|
-
);
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
// Auth0 login flow
|
|
247
|
-
await page.fill('input[name="username"]', email);
|
|
248
|
-
await page.fill('input[name="password"]', password);
|
|
249
|
-
await page.click('button[type="submit"]');
|
|
132
|
+
const HOOKS_README_TEMPLATE = `# HubLaunch Hooks
|
|
250
133
|
|
|
251
|
-
|
|
252
|
-
await page.waitForURL(\`\${context.deploymentUrl}/**\`, {
|
|
253
|
-
timeout: 10000
|
|
254
|
-
});
|
|
134
|
+
This directory contains project-specific hooks that extend HubLaunch functionality.
|
|
255
135
|
|
|
256
|
-
|
|
257
|
-
console.log("Browser will remain open for testing.");
|
|
136
|
+
\`beforeLaunch.ts\` and \`afterMerge.ts\` are generated with a working local-only example already active, plus commented-out Slack and database-branch (Neon+Vercel) examples you can uncomment and adapt.
|
|
258
137
|
|
|
259
|
-
|
|
260
|
-
console.error("â Login failed:", error);
|
|
261
|
-
await browser.close();
|
|
262
|
-
process.exit(1);
|
|
263
|
-
}
|
|
264
|
-
}
|
|
138
|
+
## Available Hooks
|
|
265
139
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
// await browser.close();
|
|
286
|
-
|
|
287
|
-
} catch (error) {
|
|
288
|
-
console.error("â Preview setup failed:", error);
|
|
289
|
-
await browser.close();
|
|
290
|
-
process.exit(1);
|
|
291
|
-
}
|
|
292
|
-
}
|
|
140
|
+
### \`beforeLaunch.ts\`
|
|
141
|
+
Runs **before** \`hula launch\` sends its launch request, so a project can
|
|
142
|
+
provision a freshly-created, per-launch resource (e.g. a database branch) and
|
|
143
|
+
inject its connection string into the container.
|
|
144
|
+
|
|
145
|
+
- On success it must print \`{"envVars": {"KEY": "value", ...}}\` to **stdout**.
|
|
146
|
+
Those values are merged into the launch request's \`envVars\`, taking
|
|
147
|
+
precedence over any \`.env\`-sourced values with the same key.
|
|
148
|
+
- **A nonzero exit, or stdout that is not valid \`{"envVars": {...}}\` JSON,
|
|
149
|
+
aborts the launch** â no request is sent. (Launching against the wrong
|
|
150
|
+
resource, e.g. a shared database, is worse than not launching.)
|
|
151
|
+
- **stderr streams live** to your terminal, so you can watch the hook's
|
|
152
|
+
progress (it may make several seconds of API calls).
|
|
153
|
+
- Printing nothing, \`{}\`, or \`{"envVars": {}}\` is a valid no-op â the launch
|
|
154
|
+
proceeds with no injected variables.
|
|
155
|
+
- The hook is responsible for its own **find-or-create idempotency**: a
|
|
156
|
+
kill-and-relaunch runs \`beforeLaunch\` again with the same context, so key
|
|
157
|
+
your resource off \`issueName\`/\`planPath\` and reuse an existing resource
|
|
158
|
+
rather than creating a duplicate.
|
|
293
159
|
|
|
294
|
-
|
|
295
|
-
|
|
160
|
+
**Context Provided**:
|
|
161
|
+
\`\`\`typescript
|
|
162
|
+
{
|
|
163
|
+
issueName: string; // The launch's issue/plan name
|
|
164
|
+
planPath: string; // Path to the plan being launched
|
|
296
165
|
}
|
|
297
|
-
|
|
166
|
+
\`\`\`
|
|
298
167
|
|
|
299
|
-
|
|
168
|
+
---
|
|
300
169
|
|
|
301
|
-
|
|
170
|
+
### \`afterMerge.ts\`
|
|
171
|
+
Runs **after** \`hula approve\` successfully merges a PR. Use it to clean up
|
|
172
|
+
whatever a \`beforeLaunch\` hook provisioned (e.g. delete the per-launch
|
|
173
|
+
database branch).
|
|
302
174
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
- **Authentication** (Clerk, Auth0, custom login flows)
|
|
306
|
-
- **Browser automation** (Playwright-based interactions)
|
|
307
|
-
- **Test data seeding** (API calls to populate data)
|
|
308
|
-
- **Environment setup** (configure local state)
|
|
175
|
+
- **Best-effort**: a failure only logs a warning â it never fails the merge,
|
|
176
|
+
which has already completed by the time this hook runs.
|
|
309
177
|
|
|
310
178
|
**Context Provided**:
|
|
311
179
|
\`\`\`typescript
|
|
312
180
|
{
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
prUrl?: string; // PR URL
|
|
181
|
+
issueNumber: number; // Associated issue number
|
|
182
|
+
prNumber: number; // Merged PR number
|
|
183
|
+
prTitle: string; // Merged PR title
|
|
184
|
+
branch: string; // The merged PR's source branch
|
|
318
185
|
}
|
|
319
186
|
\`\`\`
|
|
320
187
|
|
|
321
|
-
**Usage**:
|
|
322
|
-
- Configured in \`.hublaunch/hublaunch.config.ts\`
|
|
323
|
-
- Runs automatically when you run \`hula preview\`
|
|
324
|
-
- Runs in **background** (non-blocking)
|
|
325
|
-
|
|
326
188
|
---
|
|
327
189
|
|
|
328
190
|
## Creating Custom Hooks
|
|
@@ -372,11 +234,8 @@ export const config = {
|
|
|
372
234
|
// ... other config ...
|
|
373
235
|
|
|
374
236
|
hooks: {
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
afterPreview: ".hublaunch/hooks/afterPreview.ts", // optional
|
|
378
|
-
beforeMerge: ".hublaunch/hooks/beforeMerge.ts", // optional
|
|
379
|
-
afterMerge: ".hublaunch/hooks/afterMerge.ts", // optional
|
|
237
|
+
beforeLaunch: ".hublaunch/hooks/beforeLaunch.ts", // optional â inject per-launch env vars
|
|
238
|
+
afterMerge: ".hublaunch/hooks/afterMerge.ts", // optional â cleanup after merge
|
|
380
239
|
},
|
|
381
240
|
};
|
|
382
241
|
\`\`\`
|
|
@@ -409,10 +268,10 @@ Run hook manually for testing:
|
|
|
409
268
|
|
|
410
269
|
\`\`\`bash
|
|
411
270
|
# Test with sample context
|
|
412
|
-
tsx .hublaunch/hooks/
|
|
271
|
+
tsx .hublaunch/hooks/beforeLaunch.ts '{"issueName":"my-feature","planPath":".hublaunch/plans/my-feature.md"}'
|
|
413
272
|
|
|
414
273
|
# Enable debug mode
|
|
415
|
-
DEBUG=true tsx .hublaunch/hooks/
|
|
274
|
+
DEBUG=true tsx .hublaunch/hooks/beforeLaunch.ts '{"issueName":"my-feature","planPath":".hublaunch/plans/my-feature.md"}'
|
|
416
275
|
\`\`\`
|
|
417
276
|
|
|
418
277
|
---
|
|
@@ -423,6 +282,182 @@ DEBUG=true tsx .hublaunch/hooks/deploymentStartupScript.ts '{"deploymentUrl":"ht
|
|
|
423
282
|
- **Examples**: https://github.com/YizYah/hub-launch/tree/main/examples
|
|
424
283
|
- **Support**: https://github.com/YizYah/hub-launch/issues
|
|
425
284
|
`;
|
|
285
|
+
const BEFORE_LAUNCH_HOOK_TEMPLATE = `#!/usr/bin/env tsx
|
|
286
|
+
/**
|
|
287
|
+
* beforeLaunch hook â runs before \`hula launch\` sends its launch request.
|
|
288
|
+
* See ./README.md for the full stdout/stderr contract.
|
|
289
|
+
*
|
|
290
|
+
* Context: { issueName: string; planPath: string }
|
|
291
|
+
*/
|
|
292
|
+
|
|
293
|
+
interface BeforeLaunchContext {
|
|
294
|
+
issueName: string;
|
|
295
|
+
planPath: string;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
async function main() {
|
|
299
|
+
const context: BeforeLaunchContext = JSON.parse(process.argv[2] ?? '{}');
|
|
300
|
+
|
|
301
|
+
// -----------------------------------------------------------------------
|
|
302
|
+
// ACTIVE EXAMPLE â runs by default. Local-only, no external services or
|
|
303
|
+
// credentials required. Demonstrates the {"envVars": {...}} stdout
|
|
304
|
+
// contract by injecting a LAUNCH_TIMESTAMP env var into the container.
|
|
305
|
+
// Remove this block once you add your own logic below.
|
|
306
|
+
// -----------------------------------------------------------------------
|
|
307
|
+
console.error(\`[beforeLaunch] Launching "\${context.issueName}"...\`);
|
|
308
|
+
const envVars: Record<string, string> = { LAUNCH_TIMESTAMP: new Date().toISOString() };
|
|
309
|
+
console.error(\`[beforeLaunch] Injecting LAUNCH_TIMESTAMP=\${envVars.LAUNCH_TIMESTAMP}\`);
|
|
310
|
+
|
|
311
|
+
// -----------------------------------------------------------------------
|
|
312
|
+
// EXAMPLE IDEA â Slack notification. Uncomment and set SLACK_WEBHOOK_URL
|
|
313
|
+
// to post a message when a launch starts.
|
|
314
|
+
// -----------------------------------------------------------------------
|
|
315
|
+
// const webhookUrl = process.env.SLACK_WEBHOOK_URL;
|
|
316
|
+
// if (webhookUrl) {
|
|
317
|
+
// await fetch(webhookUrl, {
|
|
318
|
+
// method: 'POST',
|
|
319
|
+
// headers: { 'Content-Type': 'application/json' },
|
|
320
|
+
// body: JSON.stringify({ text: \`đ Launching *\${context.issueName}*\` }),
|
|
321
|
+
// });
|
|
322
|
+
// console.error('[beforeLaunch] Slack notification sent.');
|
|
323
|
+
// }
|
|
324
|
+
|
|
325
|
+
// -----------------------------------------------------------------------
|
|
326
|
+
// EXAMPLE IDEA â per-launch database branch (Neon + Vercel). Provisions
|
|
327
|
+
// an isolated Postgres branch and injects its connection string, so each
|
|
328
|
+
// launch gets its own database instead of sharing one. Requires
|
|
329
|
+
// NEON_API_KEY, NEON_PROJECT_ID, VERCEL_TOKEN, VERCEL_PROJECT_ID.
|
|
330
|
+
// Find-or-create by name is required (this hook re-runs on a
|
|
331
|
+
// kill-and-relaunch with the same issueName) â never log the actual
|
|
332
|
+
// connection string or tokens.
|
|
333
|
+
// -----------------------------------------------------------------------
|
|
334
|
+
// const branchName = \`preview/\${context.issueName}\`;
|
|
335
|
+
// const branches = await fetch(
|
|
336
|
+
// \`https://console.neon.tech/api/v2/projects/\${process.env.NEON_PROJECT_ID}/branches\`,
|
|
337
|
+
// { headers: { Authorization: \`Bearer \${process.env.NEON_API_KEY}\` } },
|
|
338
|
+
// ).then((r) => r.json());
|
|
339
|
+
// // ...find branchName in branches.branches; if absent, POST to create one
|
|
340
|
+
// // off the default branch, then read the connection string back.
|
|
341
|
+
// // let connectionString = /* from the found or created branch */ '';
|
|
342
|
+
// await fetch(
|
|
343
|
+
// \`https://api.vercel.com/v10/projects/\${process.env.VERCEL_PROJECT_ID}/env\`,
|
|
344
|
+
// {
|
|
345
|
+
// method: 'POST',
|
|
346
|
+
// headers: {
|
|
347
|
+
// Authorization: \`Bearer \${process.env.VERCEL_TOKEN}\`,
|
|
348
|
+
// 'Content-Type': 'application/json',
|
|
349
|
+
// },
|
|
350
|
+
// body: JSON.stringify({
|
|
351
|
+
// key: 'DATABASE_URL',
|
|
352
|
+
// value: /* connectionString from above */ '',
|
|
353
|
+
// target: ['preview'],
|
|
354
|
+
// gitBranch: context.issueName,
|
|
355
|
+
// type: 'encrypted',
|
|
356
|
+
// }),
|
|
357
|
+
// },
|
|
358
|
+
// );
|
|
359
|
+
// envVars.DATABASE_URL = /* connectionString from above */ '';
|
|
360
|
+
// console.error('[beforeLaunch] Neon branch + Vercel env var provisioned.');
|
|
361
|
+
|
|
362
|
+
console.log(JSON.stringify({ envVars }));
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
main().catch((error) => {
|
|
366
|
+
console.error('[beforeLaunch] Hook failed:', error instanceof Error ? error.message : String(error));
|
|
367
|
+
process.exit(1);
|
|
368
|
+
});
|
|
369
|
+
`;
|
|
370
|
+
const AFTER_MERGE_HOOK_TEMPLATE = `#!/usr/bin/env tsx
|
|
371
|
+
/**
|
|
372
|
+
* afterMerge hook â runs after \`hula approve\` successfully merges a PR.
|
|
373
|
+
* Best-effort: a failure only logs a warning, it never fails the merge.
|
|
374
|
+
* See ./README.md for the full contract.
|
|
375
|
+
*
|
|
376
|
+
* Context: { issueNumber: number; prNumber: number; prTitle: string; branch: string }
|
|
377
|
+
*/
|
|
378
|
+
|
|
379
|
+
interface AfterMergeContext {
|
|
380
|
+
issueNumber: number;
|
|
381
|
+
prNumber: number;
|
|
382
|
+
prTitle: string;
|
|
383
|
+
branch: string;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
async function main() {
|
|
387
|
+
const context: AfterMergeContext = JSON.parse(process.argv[2] ?? '{}');
|
|
388
|
+
|
|
389
|
+
// -----------------------------------------------------------------------
|
|
390
|
+
// ACTIVE EXAMPLE â runs by default. Local-only: just logs what merged.
|
|
391
|
+
// Remove this block once you add your own cleanup logic below.
|
|
392
|
+
// -----------------------------------------------------------------------
|
|
393
|
+
console.error(
|
|
394
|
+
\`[afterMerge] PR #\${context.prNumber} "\${context.prTitle}" merged (issue #\${context.issueNumber}, branch \${context.branch}).\`,
|
|
395
|
+
);
|
|
396
|
+
|
|
397
|
+
// -----------------------------------------------------------------------
|
|
398
|
+
// EXAMPLE IDEA â Slack notification. Uncomment and set SLACK_WEBHOOK_URL.
|
|
399
|
+
// -----------------------------------------------------------------------
|
|
400
|
+
// const webhookUrl = process.env.SLACK_WEBHOOK_URL;
|
|
401
|
+
// if (webhookUrl) {
|
|
402
|
+
// await fetch(webhookUrl, {
|
|
403
|
+
// method: 'POST',
|
|
404
|
+
// headers: { 'Content-Type': 'application/json' },
|
|
405
|
+
// body: JSON.stringify({ text: \`â
Merged PR #\${context.prNumber}: \${context.prTitle}\` }),
|
|
406
|
+
// });
|
|
407
|
+
// console.error('[afterMerge] Slack notification sent.');
|
|
408
|
+
// }
|
|
409
|
+
|
|
410
|
+
// -----------------------------------------------------------------------
|
|
411
|
+
// EXAMPLE IDEA â tear down the per-launch database branch (Neon + Vercel)
|
|
412
|
+
// that a matching beforeLaunch.ts provisioned. Requires NEON_API_KEY,
|
|
413
|
+
// NEON_PROJECT_ID, VERCEL_TOKEN, VERCEL_PROJECT_ID. Treat "not found" as
|
|
414
|
+
// already-clean: log a warning and continue, don't throw â this hook may
|
|
415
|
+
// re-run, or the launch may have been killed before beforeLaunch ran.
|
|
416
|
+
// -----------------------------------------------------------------------
|
|
417
|
+
// const branchName = \`preview/\${context.branch}\`;
|
|
418
|
+
// // ...GET the Neon branch by name; if found, DELETE it; if not found,
|
|
419
|
+
// // console.error a warning and continue.
|
|
420
|
+
// // ...GET the Vercel branch-scoped DATABASE_URL env var by gitBranch; if
|
|
421
|
+
// // found, DELETE it; if not found, console.error a warning and continue.
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
main().catch((error) => {
|
|
425
|
+
console.error('[afterMerge] Hook failed (non-fatal):', error instanceof Error ? error.message : String(error));
|
|
426
|
+
});
|
|
427
|
+
`;
|
|
428
|
+
// Hook keys always written as a real (uncommented) config line by init. Each
|
|
429
|
+
// key gets its existing/configured value if the merged config already has one,
|
|
430
|
+
// otherwise the default path â which points at the working example file that
|
|
431
|
+
// generateHookTemplates() scaffolds. Order here is the order their lines are
|
|
432
|
+
// written to the generated config. Deliberately not every HooksSchema key â
|
|
433
|
+
// beforePreview, afterPreview, beforeMerge, and deploymentShutdown are valid
|
|
434
|
+
// config keys but are not suggested by init (see the fallback loop below,
|
|
435
|
+
// which still preserves any of them if a config already has one set).
|
|
436
|
+
const HOOK_KEY_DEFAULTS = [
|
|
437
|
+
{ key: 'beforeLaunch', defaultPath: '.hublaunch/hooks/beforeLaunch.ts' },
|
|
438
|
+
{ key: 'afterMerge', defaultPath: '.hublaunch/hooks/afterMerge.ts' },
|
|
439
|
+
];
|
|
440
|
+
/**
|
|
441
|
+
* Build the `hooks: { ... }` config section. Each key in HOOK_KEY_DEFAULTS is
|
|
442
|
+
* always written as a real, uncommented line: the existing/merged config's
|
|
443
|
+
* value if it already has one (never overridden), otherwise the default path
|
|
444
|
+
* to the working example file that generateHookTemplates() scaffolds. Any
|
|
445
|
+
* other configured hook key (valid per HooksSchema, just not advertised) is
|
|
446
|
+
* still preserved as a real line â it is never dropped.
|
|
447
|
+
*/
|
|
448
|
+
export function buildHooksConfigSection(hooks) {
|
|
449
|
+
const advertisedKeys = new Set(HOOK_KEY_DEFAULTS.map((d) => d.key));
|
|
450
|
+
const lines = HOOK_KEY_DEFAULTS.map(({ key, defaultPath }) => {
|
|
451
|
+
const value = hooks?.[key] ?? defaultPath;
|
|
452
|
+
return ` ${key}: ${JSON.stringify(value)},`;
|
|
453
|
+
});
|
|
454
|
+
for (const [key, value] of Object.entries(hooks ?? {})) {
|
|
455
|
+
if (value && !advertisedKeys.has(key)) {
|
|
456
|
+
lines.push(` ${key}: ${JSON.stringify(value)},`);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
return `\n\n // Lifecycle hooks\n hooks: {\n${lines.join('\n')}\n },`;
|
|
460
|
+
}
|
|
426
461
|
const ADAPTERS_README_TEMPLATE = `# HubLaunch Adapters
|
|
427
462
|
|
|
428
463
|
This directory contains custom service adapters for extending HubLaunch functionality.
|
|
@@ -558,33 +593,29 @@ Templates are automatically detected and used by HubLaunch when creating issues
|
|
|
558
593
|
\`\`\`
|
|
559
594
|
`;
|
|
560
595
|
/**
|
|
561
|
-
* Generate hook
|
|
596
|
+
* Generate hook directory structure with documentation
|
|
562
597
|
*/
|
|
563
|
-
export async function generateHookTemplates(projectRoot
|
|
598
|
+
export async function generateHookTemplates(projectRoot) {
|
|
564
599
|
const hooksDir = join(projectRoot, '.hublaunch/hooks');
|
|
565
600
|
// Create hooks directory
|
|
566
601
|
await mkdir(hooksDir, { recursive: true });
|
|
567
602
|
logger.success(`Created directory: .hublaunch/hooks/`);
|
|
568
|
-
// Generate deploymentStartupScript.ts template based on auth provider
|
|
569
|
-
// Only create if auth provider is configured (not "none"). An existing script
|
|
570
|
-
// is never overwritten â the user may have customized it.
|
|
571
|
-
if (authProvider !== 'none') {
|
|
572
|
-
const startupScriptPath = join(hooksDir, 'deploymentStartupScript.ts');
|
|
573
|
-
if (!existsSync(startupScriptPath)) {
|
|
574
|
-
const template = getDeploymentStartupTemplate(authProvider);
|
|
575
|
-
await writeFile(startupScriptPath, template, 'utf-8');
|
|
576
|
-
logger.success(`Created: .hublaunch/hooks/deploymentStartupScript.ts`);
|
|
577
|
-
}
|
|
578
|
-
else {
|
|
579
|
-
logger.info('Keeping existing deploymentStartupScript.ts');
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
603
|
// Generate README for hooks
|
|
583
604
|
const readmePath = join(hooksDir, 'README.md');
|
|
584
605
|
if (!existsSync(readmePath)) {
|
|
585
606
|
await writeFile(readmePath, HOOKS_README_TEMPLATE, 'utf-8');
|
|
586
607
|
logger.success(`Created: .hublaunch/hooks/README.md`);
|
|
587
608
|
}
|
|
609
|
+
const beforeLaunchPath = join(hooksDir, 'beforeLaunch.ts');
|
|
610
|
+
if (!existsSync(beforeLaunchPath)) {
|
|
611
|
+
await writeFile(beforeLaunchPath, BEFORE_LAUNCH_HOOK_TEMPLATE, 'utf-8');
|
|
612
|
+
logger.success(`Created: .hublaunch/hooks/beforeLaunch.ts`);
|
|
613
|
+
}
|
|
614
|
+
const afterMergePath = join(hooksDir, 'afterMerge.ts');
|
|
615
|
+
if (!existsSync(afterMergePath)) {
|
|
616
|
+
await writeFile(afterMergePath, AFTER_MERGE_HOOK_TEMPLATE, 'utf-8');
|
|
617
|
+
logger.success(`Created: .hublaunch/hooks/afterMerge.ts`);
|
|
618
|
+
}
|
|
588
619
|
logger.blank();
|
|
589
620
|
logger.info('đ See .hublaunch/hooks/README.md for hook documentation');
|
|
590
621
|
}
|
|
@@ -1076,12 +1107,6 @@ export async function executeInit(options) {
|
|
|
1076
1107
|
},
|
|
1077
1108
|
};
|
|
1078
1109
|
}
|
|
1079
|
-
// Add hooks if configured
|
|
1080
|
-
if (authProvider !== 'none') {
|
|
1081
|
-
baseConfig.hooks = {
|
|
1082
|
-
deploymentStartup: '.hublaunch/hooks/deploymentStartupScript.ts',
|
|
1083
|
-
};
|
|
1084
|
-
}
|
|
1085
1110
|
// Merge with existing config (existing values take precedence)
|
|
1086
1111
|
const finalConfig = existingConfig
|
|
1087
1112
|
? mergeConfigs(existingConfig, baseConfig)
|
|
@@ -1104,10 +1129,11 @@ export async function executeInit(options) {
|
|
|
1104
1129
|
const cliVersion = getCliVersion();
|
|
1105
1130
|
if (cliVersion)
|
|
1106
1131
|
finalConfig.version = cliVersion;
|
|
1107
|
-
// Generate hooks config section
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1132
|
+
// Generate hooks config section. Always emitted: every known hook key gets
|
|
1133
|
+
// a line â a real, uncommented value when already configured (never
|
|
1134
|
+
// overridden), otherwise a commented-out placeholder so the available hook
|
|
1135
|
+
// points are discoverable without reading docs.
|
|
1136
|
+
const hooksConfigSection = buildHooksConfigSection(finalConfig.hooks);
|
|
1111
1137
|
// Generate services config section without credentials (omitted when not configured)
|
|
1112
1138
|
const servicesConfigSection = finalConfig.services
|
|
1113
1139
|
? `\n\n // Service configuration\n services: ${JSON.stringify(finalConfig.services, null, 2).replace(/\n/g, '\n ')},`
|
|
@@ -1289,10 +1315,10 @@ ${envVarsLine}${stepsLine}${buildPreservedSection(finalConfig)}
|
|
|
1289
1315
|
logger.info(`âšī¸ .env file already exists at ${envPath} - skipping to preserve your existing configuration`);
|
|
1290
1316
|
logger.info(' Add any required environment variables manually if needed');
|
|
1291
1317
|
}
|
|
1292
|
-
// Always generate hooks directory structure (with README
|
|
1318
|
+
// Always generate hooks directory structure (with README)
|
|
1293
1319
|
logger.blank();
|
|
1294
1320
|
logger.info('Generating hooks directory structure...');
|
|
1295
|
-
await generateHookTemplates(repoRoot
|
|
1321
|
+
await generateHookTemplates(repoRoot);
|
|
1296
1322
|
// Always generate adapters directory structure
|
|
1297
1323
|
logger.blank();
|
|
1298
1324
|
logger.info('Generating adapter directory structure...');
|
|
@@ -1362,9 +1388,6 @@ ${envVarsLine}${stepsLine}${buildPreservedSection(finalConfig)}
|
|
|
1362
1388
|
logger.listItem('- TEST_USER_PASSWORD: Test user password', 2);
|
|
1363
1389
|
logger.blank();
|
|
1364
1390
|
}
|
|
1365
|
-
if (authProvider !== 'none') {
|
|
1366
|
-
logger.listItem('Customize .hublaunch/hooks/deploymentStartupScript.ts as needed', 1);
|
|
1367
|
-
}
|
|
1368
1391
|
if (showLoginHint) {
|
|
1369
1392
|
logger.listItem("Run 'hula login' to authenticate.", 1);
|
|
1370
1393
|
}
|