Skip to content

Commit 6286d38

Browse files
committed
feat: ease scaffolding inputs with hub configuration
1 parent 9b0dc1e commit 6286d38

File tree

8 files changed

+315
-27
lines changed

8 files changed

+315
-27
lines changed

docs/reference/hub-schema.md

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,52 @@ configuration:
224224
strictMode: true
225225
```
226226

227+
## Scaffold Defaults Object (Optional)
228+
229+
Default values for project scaffolding prompts. Each organization-level field is individually skipped when its default is present. Fields like `githubOrg` and `githubRunner` are always shown but pre-filled from defaults.
230+
231+
| Field | Type | Description |
232+
|-------|------|-------------|
233+
| `githubOrg` | string | GitHub organization name (pre-fills the prompt; always shown) |
234+
| `githubRunner` | string | GitHub Actions runner label or pattern. Supports `{githubOrg}` placeholder (e.g., `gmsshr-core-{githubOrg}`). Always shown, pre-filled with resolved value. |
235+
| `organizationName` | string | Organization name for LICENSE (skipped when present — value used directly) |
236+
| `internalContact` | string | Internal contact email for security/support (skipped when present) |
237+
| `legalContact` | string | Legal contact email for licensing questions (skipped when present) |
238+
| `organizationPolicyLink` | string | Organization policy URL (skipped when present) |
239+
240+
**Note:** `author`, `description`, and `tags` are NOT part of scaffold defaults. The author field is pre-filled from the user's GitHub identity instead.
241+
242+
### Runner Pattern
243+
244+
The `githubRunner` field supports a `{githubOrg}` placeholder that is resolved at scaffold time using the selected GitHub organization. Only `{githubOrg}` is supported.
245+
246+
**Example:** `gmsshr-core-{githubOrg}` with org `myorg` → `gmsshr-core-myorg`
247+
248+
### Prompt Behavior
249+
250+
| Field | Hub Default Present | Hub Default Absent |
251+
|-------|--------------------|--------------------|
252+
| `projectName` | Always asked | Always asked |
253+
| `author` | Prefilled from GitHub identity | Prefilled from GitHub identity |
254+
| `githubOrg` | Prefilled from default | Shown empty |
255+
| `githubRunner` | Prefilled with resolved pattern | Quick pick (ubuntu-latest / self-hosted / custom) |
256+
| `organizationName` | **Skipped** — hub value used | Asked |
257+
| `internalContact` | **Skipped** — hub value used | Asked |
258+
| `legalContact` | **Skipped** — hub value used | Asked |
259+
| `organizationPolicyLink` | **Skipped** — hub value used | Asked |
260+
261+
### Example
262+
263+
```yaml
264+
scaffoldDefaults:
265+
githubOrg: "myorg"
266+
githubRunner: "gmsshr-core-{githubOrg}"
267+
organizationName: "My Organization Inc."
268+
internalContact: "security@myorg.com"
269+
legalContact: "legal@myorg.com"
270+
organizationPolicyLink: "https://myorg.com/policies"
271+
```
272+
227273
## Complete Example
228274

229275
```yaml
@@ -269,6 +315,14 @@ profiles:
269315
configuration:
270316
autoSync: true
271317
syncInterval: 3600
318+
319+
scaffoldDefaults:
320+
githubOrg: "myorg"
321+
githubRunner: "gmsshr-core-{githubOrg}"
322+
organizationName: "My Organization"
323+
internalContact: "security@myorg.com"
324+
legalContact: "legal@myorg.com"
325+
organizationPolicyLink: "https://myorg.com/policies"
272326
```
273327

274328
## Validation

schemas/hub-config.schema.json

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,51 @@
285285
}
286286
}
287287
},
288+
"scaffoldDefaults": {
289+
"type": "object",
290+
"description": "Default values for project scaffolding prompts. When present, organization-level fields are applied automatically without prompting the user.",
291+
"additionalProperties": false,
292+
"properties": {
293+
"githubOrg": {
294+
"type": "string",
295+
"description": "GitHub organization name (pre-fills the githubOrg prompt)",
296+
"minLength": 1,
297+
"maxLength": 100,
298+
"pattern": "^[a-zA-Z0-9_-]+$"
299+
},
300+
"githubRunner": {
301+
"type": "string",
302+
"description": "GitHub Actions runner label or pattern. Supports {githubOrg} placeholder (e.g., 'gmsshr-core-{githubOrg}')",
303+
"minLength": 1,
304+
"maxLength": 200
305+
},
306+
"organizationName": {
307+
"type": "string",
308+
"description": "Organization name for LICENSE and documentation",
309+
"minLength": 1,
310+
"maxLength": 200
311+
},
312+
"internalContact": {
313+
"type": "string",
314+
"description": "Internal contact email for security/support inquiries",
315+
"minLength": 1,
316+
"maxLength": 200
317+
},
318+
"legalContact": {
319+
"type": "string",
320+
"description": "Legal contact email for licensing questions",
321+
"minLength": 1,
322+
"maxLength": 200
323+
},
324+
"organizationPolicyLink": {
325+
"type": "string",
326+
"description": "URL to organization policy page",
327+
"minLength": 1,
328+
"maxLength": 500,
329+
"format": "uri"
330+
}
331+
}
332+
},
288333
"engagement": {
289334
"type": "object",
290335
"description": "Engagement features configuration (telemetry, ratings, feedback)",

src/commands/ScaffoldCommand.ts

Lines changed: 70 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ import { Logger } from '../utils/logger';
55
import { TemplateEngine, TemplateContext } from '../services/TemplateEngine';
66
import { FileUtils } from '../utils/fileUtils';
77
import { generateSanitizedId } from '../utils/bundleNameUtils';
8+
import { ScaffoldDefaults } from '../types/hub';
9+
import { HubManager } from '../services/HubManager';
10+
import { resolveRunnerPattern } from '../utils/scaffoldUtils';
811
import { NpmCliWrapper } from '../utils/NpmCliWrapper';
912

1013

@@ -232,28 +235,39 @@ export class ScaffoldCommand {
232235
/**
233236
* Run the scaffold command with full UI flow
234237
*/
235-
static async runWithUI(): Promise<void> {
238+
static async runWithUI(hubManager?: HubManager): Promise<void> {
236239
const logger = Logger.getInstance();
237240

238241
try {
239242
// Step 1: Select scaffold type
240243
const scaffoldType = await ScaffoldCommand.promptForScaffoldType();
241244
if (!scaffoldType) {return;}
242245

243-
// Step 2: Handle skill creation in existing project
246+
// Step 2: Load scaffold defaults from active hub
247+
let scaffoldDefaults: ScaffoldDefaults | undefined;
248+
if (hubManager) {
249+
try {
250+
const activeHub = await hubManager.getActiveHub();
251+
scaffoldDefaults = activeHub?.config?.scaffoldDefaults;
252+
} catch {
253+
// No active hub or hub load failed — proceed without defaults
254+
}
255+
}
256+
257+
// Step 3: Handle skill creation in existing project
244258
if (scaffoldType.value === ScaffoldType.Skill && await ScaffoldCommand.handleSkillInExistingProject()) {
245259
return;
246260
}
247261

248-
// Step 3: Select target directory
262+
// Step 4: Select target directory
249263
const targetPath = await ScaffoldCommand.promptForTargetDirectory(scaffoldType.label);
250264
if (!targetPath) {return;}
251265

252-
// Step 4: Collect project details
253-
const options = await ScaffoldCommand.promptForProjectDetails(scaffoldType.value);
266+
// Step 5: Collect project details
267+
const options = await ScaffoldCommand.promptForProjectDetails(scaffoldType.value, scaffoldDefaults);
254268
if (!options) {return;}
255269

256-
// Step 5: Execute scaffold with progress
270+
// Step 6: Execute scaffold with progress
257271
await vscode.window.withProgress(
258272
{ location: vscode.ProgressLocation.Notification, title: `Scaffolding ${scaffoldType.label}...` },
259273
async () => {
@@ -262,7 +276,7 @@ export class ScaffoldCommand {
262276
}
263277
);
264278

265-
// Step 6: Post-scaffold actions
279+
// Step 7: Post-scaffold actions
266280
await ScaffoldCommand.handlePostScaffoldActions(scaffoldType.label, targetPath);
267281
} catch (error) {
268282
logger.error('Scaffold failed', error as Error);
@@ -338,7 +352,7 @@ export class ScaffoldCommand {
338352
/**
339353
* Collect project details from user input
340354
*/
341-
private static async promptForProjectDetails(type: ScaffoldType): Promise<ScaffoldOptions | undefined> {
355+
private static async promptForProjectDetails(type: ScaffoldType, scaffoldDefaults?: ScaffoldDefaults): Promise<ScaffoldOptions | undefined> {
342356
// Get project name
343357
const projectName = await vscode.window.showInputBox({
344358
prompt: 'Enter project name (optional)',
@@ -347,13 +361,11 @@ export class ScaffoldCommand {
347361
ignoreFocusOut: true
348362
});
349363

350-
// Get GitHub runner choice
351-
const githubRunner = await ScaffoldCommand.promptForGitHubRunner();
352364
let details: { description?: string; author?: string; tags?: string[] } = {};
353-
let orgDetails: { organizationName?: string; internalContact?: string; legalContact?: string; organizationPolicyLink?: string } = {};
354-
365+
let orgDetails: { author?: string; githubOrg?: string; organizationName?: string; internalContact?: string; legalContact?: string; organizationPolicyLink?: string } = {};
366+
355367
// Collect additional details if needed
356-
368+
357369
if (type === ScaffoldType.Apm) {
358370
const apmDetails = await ScaffoldCommand.promptForApmDetails();
359371
if (apmDetails) {
@@ -370,9 +382,12 @@ export class ScaffoldCommand {
370382

371383
// For GitHub type, collect organization details for InnerSource LICENSE
372384
if (type === ScaffoldType.GitHub) {
373-
orgDetails = await ScaffoldCommand.promptForOrganizationDetails();
385+
orgDetails = await ScaffoldCommand.promptForOrganizationDetails(scaffoldDefaults);
374386
}
375387

388+
// Get GitHub runner choice (always asked, prefilled from hub defaults)
389+
const githubRunner = await ScaffoldCommand.promptForGitHubRunner(scaffoldDefaults, orgDetails.githubOrg);
390+
376391
return {
377392
projectName,
378393
githubRunner,
@@ -384,7 +399,29 @@ export class ScaffoldCommand {
384399
/**
385400
* Prompt for GitHub Actions runner configuration
386401
*/
387-
private static async promptForGitHubRunner(): Promise<string> {
402+
private static async promptForGitHubRunner(scaffoldDefaults?: ScaffoldDefaults, githubOrg?: string): Promise<string> {
403+
// If hub provides a runner default, resolve the pattern and pre-fill
404+
if (scaffoldDefaults?.githubRunner) {
405+
const resolvedRunner = githubOrg
406+
? resolveRunnerPattern(scaffoldDefaults.githubRunner, githubOrg)
407+
: scaffoldDefaults.githubRunner;
408+
const customRunner = await vscode.window.showInputBox({
409+
prompt: 'Enter GitHub Actions runner label',
410+
placeHolder: 'my-runner or [self-hosted, linux, x64]',
411+
value: resolvedRunner,
412+
validateInput: (value) => {
413+
if (!value || value.trim().length === 0) {
414+
return 'Runner label cannot be empty';
415+
}
416+
return undefined;
417+
},
418+
ignoreFocusOut: true
419+
});
420+
// undefined means the user pressed Escape — use resolved default
421+
// empty string is prevented by validateInput
422+
return customRunner ?? resolvedRunner;
423+
}
424+
388425
const runnerChoice = await vscode.window.showQuickPick(
389426
[
390427
{
@@ -484,56 +521,63 @@ export class ScaffoldCommand {
484521
}
485522

486523
/**
487-
* Prompt for organization details for InnerSource LICENSE and docs
524+
* Prompt for organization details for InnerSource LICENSE and docs.
525+
* When hub scaffoldDefaults are present, individual org-level fields are
526+
* skipped (hub value used directly). Fields without a hub default are still prompted.
488527
*/
489-
private static async promptForOrganizationDetails(): Promise<{
528+
private static async promptForOrganizationDetails(
529+
scaffoldDefaults?: ScaffoldDefaults
530+
): Promise<{
490531
author?: string;
491532
githubOrg?: string;
492-
organizationName?: string;
493-
internalContact?: string;
494-
legalContact?: string;
495-
organizationPolicyLink?: string
533+
organizationName?: string;
534+
internalContact?: string;
535+
legalContact?: string;
536+
organizationPolicyLink?: string
496537
}> {
497538
const author = await vscode.window.showInputBox({
498539
prompt: 'Enter author name (for package.json)',
499540
placeHolder: 'Your Name or Team Name',
500541
ignoreFocusOut: true
501542
});
502543

544+
// githubOrg: always asked, prefilled from hub defaults if present
503545
const githubOrg = await vscode.window.showInputBox({
504546
prompt: 'Enter GitHub organization/username (for repository URLs)',
505547
placeHolder: 'your-org',
548+
value: scaffoldDefaults?.githubOrg || '',
506549
ignoreFocusOut: true
507550
});
508551

509-
const organizationName = await vscode.window.showInputBox({
552+
// Each org field: use hub default if present, otherwise prompt
553+
const organizationName = scaffoldDefaults?.organizationName ?? await vscode.window.showInputBox({
510554
prompt: 'Enter organization name (for LICENSE)',
511555
placeHolder: 'Your Organization Name',
512556
value: 'Your Organization',
513557
ignoreFocusOut: true
514558
});
515559

516-
const internalContact = await vscode.window.showInputBox({
560+
const internalContact = scaffoldDefaults?.internalContact ?? await vscode.window.showInputBox({
517561
prompt: 'Enter internal contact email (for security/support inquiries)',
518562
placeHolder: 'security@yourorg.com',
519563
value: 'security@yourorg.com',
520564
ignoreFocusOut: true
521565
});
522566

523-
const legalContact = await vscode.window.showInputBox({
567+
const legalContact = scaffoldDefaults?.legalContact ?? await vscode.window.showInputBox({
524568
prompt: 'Enter legal contact email (for licensing questions)',
525569
placeHolder: 'legal@yourorg.com',
526570
value: 'legal@yourorg.com',
527571
ignoreFocusOut: true
528572
});
529573

530-
const organizationPolicyLink = await vscode.window.showInputBox({
574+
const organizationPolicyLink = scaffoldDefaults?.organizationPolicyLink ?? await vscode.window.showInputBox({
531575
prompt: 'Enter organization policy URL (optional)',
532576
placeHolder: 'https://yourorg.com/policies',
533577
ignoreFocusOut: true
534578
});
535579

536-
return {
580+
return {
537581
author,
538582
githubOrg,
539583
organizationName,

src/extension.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -377,7 +377,7 @@ export class PromptRegistryExtension {
377377
vscode.commands.registerCommand('promptRegistry.cleanupStaleLockfileEntries', () => this.bundleCommands!.cleanupStaleLockfileEntries()),
378378

379379
// Scaffold Command - Create project structure
380-
vscode.commands.registerCommand('promptRegistry.scaffoldProject', () => ScaffoldCommand.runWithUI()),
380+
vscode.commands.registerCommand('promptRegistry.scaffoldProject', () => ScaffoldCommand.runWithUI(this.hubManager)),
381381

382382

383383
// Add Resource Command - Add individual resources

src/types/hub.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,24 @@ export interface HubReference {
2323
autoSync?: boolean;
2424
}
2525

26+
/**
27+
* Default values for scaffold prompts, provided by hub configuration
28+
*/
29+
export interface ScaffoldDefaults {
30+
/** GitHub organization name (prefills githubOrg prompt) */
31+
githubOrg?: string;
32+
/** GitHub Actions runner label or pattern (supports {githubOrg} placeholder) */
33+
githubRunner?: string;
34+
/** Organization name for LICENSE (skips prompt when present) */
35+
organizationName?: string;
36+
/** Internal contact email (skips prompt when present) */
37+
internalContact?: string;
38+
/** Legal contact email (skips prompt when present) */
39+
legalContact?: string;
40+
/** Organization policy URL (skips prompt when present) */
41+
organizationPolicyLink?: string;
42+
}
43+
2644
/**
2745
* Hub configuration structure
2846
*/
@@ -41,6 +59,9 @@ export interface HubConfig {
4159

4260
/** Optional registry configuration */
4361
configuration?: RegistryConfiguration;
62+
63+
/** Optional scaffold defaults for pre-filling project scaffolding prompts */
64+
scaffoldDefaults?: ScaffoldDefaults;
4465
}
4566

4667
/**

src/utils/scaffoldUtils.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
/**
2+
* Resolve a runner pattern by replacing {githubOrg} with the actual org value.
3+
* Only the {githubOrg} placeholder is supported.
4+
*/
5+
export function resolveRunnerPattern(pattern: string, githubOrg: string): string {
6+
return pattern.replace(/\{githubOrg\}/g, githubOrg);
7+
}

0 commit comments

Comments
 (0)