Skip to content

Commit b5246a0

Browse files
committed
Add metadata-driven defaults for init to resolve default/workspace and default/module
- remove hardcoded templatePath defaults from init workspace/module commands so metadata resolution uses .boilerplates.json (dir: default) - stop setting templatePath in init defaults; let scaffoldTemplate resolve default/workspace or default/module automatically - rely on template-scaffold metadata resolution (base dir from .boilerplates.json with fallback to legacy paths) - regenerate CLI snapshots to reflect absence of templatePath overrides - allow init (workspace/module) to work on restructuring branch without --template-path flags
1 parent 6a35ff5 commit b5246a0

15 files changed

Lines changed: 315 additions & 54 deletions

File tree

packages/cli/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ lql init --template-path ./custom-templates/module
8080

8181
**Options:**
8282

83-
- `--repo <repo>` - Template repo (default: `https://github.com/launchql/pgpm-boilerplates.git`)
83+
- `--repo <repo>` - Template repo (default: `https://github.com/constructive-io/pgpm-boilerplates.git`)
8484
- `--template-path <path>` - Template sub-path (defaults to `workspace`/`module`) or local path override
8585
- `--from-branch <branch>` - Branch/tag when cloning the template repo
8686

packages/cli/__tests__/__snapshots__/extensions.test.ts.snap

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ exports[`cmds:extension runs \`extension\` command after workspace and module se
99
"README.md",
1010
"Makefile",
1111
"LICENSE",
12+
".boilerplate.json",
1213
"__tests__/basic.test.ts",
1314
]
1415
`;

packages/cli/__tests__/__snapshots__/init.test.ts.snap

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,15 @@ exports[`cmds:init initializes module: module-only - argv 1`] = `
2323
"plpgsql",
2424
"citext",
2525
],
26+
"fromBranch": "restructuring",
2627
"fullName": "Tester",
2728
"license": "MIT",
2829
"moduleDesc": "my-module",
2930
"moduleName": "my-module",
3031
"name": "my-module",
3132
"packageIdentifier": "my-module",
32-
"repo": "https://github.com/launchql/pgpm-boilerplates.git",
33+
"repo": "https://github.com/constructive-io/pgpm-boilerplates.git",
3334
"repoName": "my-module",
34-
"templatePath": "module",
3535
"username": "tester",
3636
}
3737
`;
@@ -48,13 +48,15 @@ exports[`cmds:init initializes module: module-only - files 1`] = `
4848
".prettierrc.json",
4949
".gitignore",
5050
".eslintrc.json",
51+
".boilerplate.json",
5152
"packages/my-module/pgpm.plan",
5253
"packages/my-module/package.json",
5354
"packages/my-module/my-module.control",
5455
"packages/my-module/jest.config.js",
5556
"packages/my-module/README.md",
5657
"packages/my-module/Makefile",
5758
"packages/my-module/LICENSE",
59+
"packages/my-module/.boilerplate.json",
5860
"packages/my-module/__tests__/basic.test.ts",
5961
".github/ci.yaml",
6062
]
@@ -72,15 +74,15 @@ exports[`cmds:init initializes module: module-only - result 1`] = `
7274
"plpgsql",
7375
"citext",
7476
],
77+
"fromBranch": "restructuring",
7578
"fullName": "Tester",
7679
"license": "MIT",
7780
"moduleDesc": "my-module",
7881
"moduleName": "my-module",
7982
"name": "my-module",
8083
"packageIdentifier": "my-module",
81-
"repo": "https://github.com/launchql/pgpm-boilerplates.git",
84+
"repo": "https://github.com/constructive-io/pgpm-boilerplates.git",
8285
"repoName": "my-module",
83-
"templatePath": "module",
8486
"username": "tester",
8587
}
8688
`;
@@ -98,15 +100,15 @@ exports[`cmds:init initializes workspace: workspace - argv 1`] = `
98100
"access": "public",
99101
"cwd": "<CWD>",
100102
"email": "tester@example.com",
103+
"fromBranch": "restructuring",
101104
"fullName": "Tester",
102105
"license": "MIT",
103106
"moduleDesc": "my-workspace",
104107
"moduleName": "starter-module",
105108
"name": "my-workspace",
106109
"packageIdentifier": "my-workspace",
107-
"repo": "https://github.com/launchql/pgpm-boilerplates.git",
110+
"repo": "https://github.com/constructive-io/pgpm-boilerplates.git",
108111
"repoName": "my-workspace",
109-
"templatePath": "workspace",
110112
"username": "tester",
111113
"workspace": true,
112114
}
@@ -124,6 +126,7 @@ exports[`cmds:init initializes workspace: workspace - files 1`] = `
124126
"my-workspace/.prettierrc.json",
125127
"my-workspace/.gitignore",
126128
"my-workspace/.eslintrc.json",
129+
"my-workspace/.boilerplate.json",
127130
"my-workspace/.github/ci.yaml",
128131
]
129132
`;
@@ -137,15 +140,15 @@ exports[`cmds:init initializes workspace: workspace - result 1`] = `
137140
"access": "public",
138141
"cwd": "<CWD>",
139142
"email": "tester@example.com",
143+
"fromBranch": "restructuring",
140144
"fullName": "Tester",
141145
"license": "MIT",
142146
"moduleDesc": "my-workspace",
143147
"moduleName": "starter-module",
144148
"name": "my-workspace",
145149
"packageIdentifier": "my-workspace",
146-
"repo": "https://github.com/launchql/pgpm-boilerplates.git",
150+
"repo": "https://github.com/constructive-io/pgpm-boilerplates.git",
147151
"repoName": "my-workspace",
148-
"templatePath": "workspace",
149152
"username": "tester",
150153
"workspace": true,
151154
}

packages/cli/__tests__/init.templates.test.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import path from 'path';
66

77
import { scaffoldTemplate } from '@launchql/core';
88

9-
const TEMPLATE_REPO = 'https://github.com/launchql/pgpm-boilerplates.git';
9+
const TEMPLATE_REPO = 'https://github.com/constructive-io/pgpm-boilerplates.git';
1010

1111
describe('Template scaffolding', () => {
1212
it('processes workspace template from default repo', async () => {
@@ -16,7 +16,8 @@ describe('Template scaffolding', () => {
1616
type: 'workspace',
1717
outputDir: outDir,
1818
templateRepo: TEMPLATE_REPO,
19-
templatePath: 'workspace',
19+
branch: 'restructuring', // TODO: remove after merging restructuring to main
20+
templatePath: 'default/workspace',
2021
answers: {
2122
name: 'demo-workspace',
2223
fullName: 'Tester',
@@ -40,7 +41,8 @@ describe('Template scaffolding', () => {
4041
type: 'module',
4142
outputDir: outDir,
4243
templateRepo: TEMPLATE_REPO,
43-
templatePath: 'module',
44+
branch: 'restructuring', // TODO: remove after merging restructuring to main
45+
templatePath: 'default/module',
4446
answers: {
4547
name: 'demo-module',
4648
description: 'demo module',

packages/cli/__tests__/init.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ describe('cmds:init', () => {
123123
cwd: fixture.tempDir,
124124
name: 'test-workspace-template',
125125
workspace: true,
126-
templatePath: 'workspace'
126+
templatePath: 'default/workspace'
127127
});
128128

129129
await commands(argv, prompter, {
@@ -171,7 +171,7 @@ describe('cmds:init', () => {
171171
name: 'test-module-template',
172172
moduleName: 'test-module-template',
173173
extensions: ['plpgsql'],
174-
templatePath: 'module'
174+
templatePath: 'default/module'
175175
}), prompter, {
176176
noTty: true,
177177
input: mockInput,
@@ -236,7 +236,7 @@ describe('cmds:init', () => {
236236
cwd: fixture.tempDir,
237237
name: 'test-workspace-branch',
238238
workspace: true,
239-
fromBranch: 'main'
239+
fromBranch: 'restructuring' // TODO: change back to 'main' after restructuring is merged
240240
});
241241

242242
await commands(argv, prompter, {

packages/cli/src/commands.ts

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,28 @@ const createCommandMap = (skipPgTeardown: boolean = false): Record<string, Funct
2222
};
2323

2424
export const commands = async (argv: Partial<ParsedArgs>, prompter: Inquirerer, options: CLIOptions & { skipPgTeardown?: boolean }) => {
25+
let { first: command, newArgv } = extractFirst(argv);
26+
27+
// Run update check early so it shows on help/version paths too
28+
try {
29+
const pkg = readAndParsePackageJson();
30+
await checkForUpdates({
31+
command: command || 'help',
32+
pkgName: pkg.name,
33+
pkgVersion: pkg.version,
34+
toolName: 'lql',
35+
key: pkg.name,
36+
updateCommand: `Run npm i -g ${pkg.name}@latest to upgrade.`
37+
});
38+
} catch {
39+
// ignore update check failures
40+
}
41+
2542
if (argv.version || argv.v) {
2643
const pkg = readAndParsePackageJson();
2744
console.log(pkg.version);
2845
process.exit(0);
2946
}
30-
let { first: command, newArgv } = extractFirst(argv);
3147

3248
// Show usage if explicitly requested but no command specified
3349
if ((argv.help || argv.h || command === 'help') && !command) {
@@ -62,20 +78,6 @@ export const commands = async (argv: Partial<ParsedArgs>, prompter: Inquirerer,
6278
command = answer.command;
6379
}
6480

65-
try {
66-
const pkg = readAndParsePackageJson();
67-
await checkForUpdates({
68-
command,
69-
pkgName: pkg.name,
70-
pkgVersion: pkg.version,
71-
toolName: 'lql',
72-
key: pkg.name,
73-
updateCommand: `Run npm i -g ${pkg.name}@latest to upgrade.`
74-
});
75-
} catch {
76-
// ignore update check failures
77-
}
78-
7981
// Prompt for working directory
8082
newArgv = await prompter.prompt(newArgv, [
8183
{

packages/cli/test-utils/init-argv.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ export const withInitDefaults = (argv: ParsedArgs, defaultRepo: string = DEFAULT
2626
return {
2727
...args,
2828
repo: args.repo ?? defaultRepo,
29-
templatePath: args.templatePath ?? (args.workspace ? 'workspace' : 'module')
29+
// TODO: remove fromBranch after merging restructuring to main
30+
fromBranch: args.fromBranch ?? 'restructuring'
31+
// Don't set default templatePath - let scaffoldTemplate use metadata-driven resolution from .boilerplates.json
3032
};
3133
};
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import fs from 'fs';
2+
import path from 'path';
3+
4+
import {
5+
BoilerplateConfig,
6+
BoilerplatesRootConfig,
7+
ScannedBoilerplate
8+
} from './boilerplate-types';
9+
10+
/**
11+
* Read the root `.boilerplates.json` configuration from a template repository.
12+
* This file specifies the default directory containing boilerplate templates.
13+
*
14+
* @param templateDir - The root directory of the template repository
15+
* @returns The root config or null if not found
16+
*/
17+
export function readBoilerplatesConfig(templateDir: string): BoilerplatesRootConfig | null {
18+
const configPath = path.join(templateDir, '.boilerplates.json');
19+
if (fs.existsSync(configPath)) {
20+
try {
21+
const content = fs.readFileSync(configPath, 'utf-8');
22+
return JSON.parse(content) as BoilerplatesRootConfig;
23+
} catch {
24+
return null;
25+
}
26+
}
27+
return null;
28+
}
29+
30+
/**
31+
* Read the `.boilerplate.json` configuration from a boilerplate directory.
32+
* This file specifies the boilerplate type and questions.
33+
*
34+
* @param boilerplatePath - The path to the boilerplate directory
35+
* @returns The boilerplate config or null if not found
36+
*/
37+
export function readBoilerplateConfig(boilerplatePath: string): BoilerplateConfig | null {
38+
const jsonPath = path.join(boilerplatePath, '.boilerplate.json');
39+
40+
if (fs.existsSync(jsonPath)) {
41+
try {
42+
const content = fs.readFileSync(jsonPath, 'utf-8');
43+
return JSON.parse(content) as BoilerplateConfig;
44+
} catch {
45+
return null;
46+
}
47+
}
48+
49+
return null;
50+
}
51+
52+
/**
53+
* Scan a base directory for boilerplate templates.
54+
* Each subdirectory with a `.boilerplate.json` file is considered a boilerplate.
55+
*
56+
* @param baseDir - The directory to scan (e.g., "default/")
57+
* @returns Array of scanned boilerplates with their configurations
58+
*/
59+
export function scanBoilerplates(baseDir: string): ScannedBoilerplate[] {
60+
const boilerplates: ScannedBoilerplate[] = [];
61+
62+
if (!fs.existsSync(baseDir)) {
63+
return boilerplates;
64+
}
65+
66+
const entries = fs.readdirSync(baseDir, { withFileTypes: true });
67+
68+
for (const entry of entries) {
69+
if (!entry.isDirectory()) {
70+
continue;
71+
}
72+
73+
const boilerplatePath = path.join(baseDir, entry.name);
74+
const config = readBoilerplateConfig(boilerplatePath);
75+
76+
if (config) {
77+
boilerplates.push({
78+
name: entry.name,
79+
path: boilerplatePath,
80+
type: config.type ?? 'module',
81+
questions: config.questions
82+
});
83+
}
84+
}
85+
86+
return boilerplates;
87+
}
88+
89+
/**
90+
* Find a boilerplate by type within a scanned list.
91+
*
92+
* @param boilerplates - Array of scanned boilerplates
93+
* @param type - The type to find ('workspace' or 'module')
94+
* @returns The matching boilerplate or undefined
95+
*/
96+
export function findBoilerplateByType(
97+
boilerplates: ScannedBoilerplate[],
98+
type: 'workspace' | 'module'
99+
): ScannedBoilerplate | undefined {
100+
return boilerplates.find((bp) => bp.type === type);
101+
}
102+
103+
/**
104+
* Resolve the base directory for boilerplates in a template repository.
105+
* Uses `.boilerplates.json` if present, otherwise returns empty string.
106+
*
107+
* @param templateDir - The root directory of the template repository
108+
* @returns The resolved base directory path
109+
*/
110+
export function resolveBoilerplateBaseDir(templateDir: string): string {
111+
const rootConfig = readBoilerplatesConfig(templateDir);
112+
if (rootConfig?.dir) {
113+
return path.join(templateDir, rootConfig.dir);
114+
}
115+
return templateDir;
116+
}
117+

0 commit comments

Comments
 (0)