@libria/scaffold 0.2.1 → 0.3.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/.clean-publish.hash +1 -1
- package/README.md +14 -9
- package/dist/cli/cli.mjs +439 -3
- package/dist/cli/cli.mjs.map +1 -1
- package/dist/cli/index.cjs +217 -2
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.d.cts +2 -39
- package/dist/cli/index.d.cts.map +1 -0
- package/dist/cli/index.d.mts +2 -39
- package/dist/cli/index.d.mts.map +1 -0
- package/dist/cli/index.mjs +175 -2
- package/dist/cli/index.mjs.map +1 -1
- package/package.json +5 -3
package/.clean-publish.hash
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
2e6dfba476fdf1b53ab57bab7c8f2964b8b5cc592a2f63346b8b1677f1e28773
|
package/README.md
CHANGED
|
@@ -262,7 +262,7 @@ templates/
|
|
|
262
262
|
### Step 2: Define Types (types.ts)
|
|
263
263
|
|
|
264
264
|
```typescript
|
|
265
|
-
import { ScaffoldTemplatePluginOptions } from "@libria/scaffold";
|
|
265
|
+
import { ScaffoldTemplatePluginOptions } from "@libria/scaffold-core";
|
|
266
266
|
|
|
267
267
|
export type MyTemplateOptions = ScaffoldTemplatePluginOptions & {
|
|
268
268
|
packageName: string;
|
|
@@ -279,7 +279,7 @@ import { fileURLToPath } from 'url';
|
|
|
279
279
|
import fs from 'fs-extra';
|
|
280
280
|
import { input, confirm } from '@inquirer/prompts';
|
|
281
281
|
import { definePlugin } from '@libria/plugin-loader';
|
|
282
|
-
import { SCAFFOLD_TEMPLATE_PLUGIN_TYPE, ScaffoldTemplatePlugin } from '@libria/scaffold';
|
|
282
|
+
import { SCAFFOLD_TEMPLATE_PLUGIN_TYPE, ScaffoldTemplatePlugin } from '@libria/scaffold-core';
|
|
283
283
|
import { MyTemplateOptions } from './types';
|
|
284
284
|
|
|
285
285
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -389,20 +389,25 @@ This compiles your template plugins to the `dist/templates` directory.
|
|
|
389
389
|
|
|
390
390
|
## Development
|
|
391
391
|
|
|
392
|
+
This package is part of the [LibriaForge Scaffold monorepo](../README.md). From the repo root:
|
|
393
|
+
|
|
392
394
|
```bash
|
|
393
|
-
# Install dependencies
|
|
395
|
+
# Install all dependencies
|
|
394
396
|
npm install
|
|
395
397
|
|
|
396
|
-
# Build
|
|
398
|
+
# Build all packages
|
|
397
399
|
npm run build
|
|
398
400
|
|
|
399
|
-
#
|
|
400
|
-
npm run
|
|
401
|
+
# Run all tests
|
|
402
|
+
npm run test
|
|
403
|
+
```
|
|
401
404
|
|
|
402
|
-
|
|
403
|
-
npm test
|
|
405
|
+
Or work on the CLI package directly:
|
|
404
406
|
|
|
405
|
-
|
|
407
|
+
```bash
|
|
408
|
+
cd cli
|
|
409
|
+
npm run build
|
|
410
|
+
npm test
|
|
406
411
|
npm run clean
|
|
407
412
|
```
|
|
408
413
|
|
package/dist/cli/cli.mjs
CHANGED
|
@@ -1,4 +1,440 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import path from "path";
|
|
2
|
+
import { fileURLToPath } from "url";
|
|
3
|
+
import { PluginManager } from "@libria/plugin-loader";
|
|
4
|
+
import { InteractiveCommand, Option } from "interactive-commander";
|
|
5
|
+
import fs from "fs/promises";
|
|
6
|
+
import { execFile } from "node:child_process";
|
|
7
|
+
import { promisify } from "node:util";
|
|
8
|
+
import * as readline from "node:readline";
|
|
9
|
+
import { SCAFFOLD_TEMPLATE_PLUGIN_TYPE } from "@libria/scaffold-core";
|
|
10
|
+
|
|
11
|
+
//#region src/config.ts
|
|
12
|
+
const CONFIG_FILENAME = ".lbscaffold";
|
|
13
|
+
/**
|
|
14
|
+
* Find the config file by searching up the directory tree
|
|
15
|
+
*/
|
|
16
|
+
async function findConfigPath(startDir = process.cwd()) {
|
|
17
|
+
let currentDir = startDir;
|
|
18
|
+
while (true) {
|
|
19
|
+
const configPath = path.join(currentDir, CONFIG_FILENAME);
|
|
20
|
+
try {
|
|
21
|
+
await fs.access(configPath);
|
|
22
|
+
return configPath;
|
|
23
|
+
} catch {
|
|
24
|
+
const parentDir = path.dirname(currentDir);
|
|
25
|
+
if (parentDir === currentDir) return null;
|
|
26
|
+
currentDir = parentDir;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Get the default config path in the current directory
|
|
32
|
+
*/
|
|
33
|
+
function getDefaultConfigPath() {
|
|
34
|
+
return path.join(process.cwd(), CONFIG_FILENAME);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Load the config file
|
|
38
|
+
*/
|
|
39
|
+
async function loadConfig(configPath) {
|
|
40
|
+
const resolvedPath = configPath ?? await findConfigPath();
|
|
41
|
+
if (!resolvedPath) return {};
|
|
42
|
+
try {
|
|
43
|
+
const content = await fs.readFile(resolvedPath, "utf-8");
|
|
44
|
+
return JSON.parse(content);
|
|
45
|
+
} catch (error) {
|
|
46
|
+
if (error.code === "ENOENT") return {};
|
|
47
|
+
throw new Error(`Failed to load config from ${resolvedPath}: ${error.message}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Save the config file
|
|
52
|
+
*/
|
|
53
|
+
async function saveConfig(config, configPath) {
|
|
54
|
+
const resolvedPath = configPath ?? getDefaultConfigPath();
|
|
55
|
+
await fs.writeFile(resolvedPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Initialize a new config file
|
|
59
|
+
*/
|
|
60
|
+
async function initConfig(configPath) {
|
|
61
|
+
const resolvedPath = configPath ?? getDefaultConfigPath();
|
|
62
|
+
try {
|
|
63
|
+
await fs.access(resolvedPath);
|
|
64
|
+
throw new Error(`Config file already exists at ${resolvedPath}`);
|
|
65
|
+
} catch (error) {
|
|
66
|
+
if (error.code !== "ENOENT") throw error;
|
|
67
|
+
}
|
|
68
|
+
await saveConfig({
|
|
69
|
+
plugins: ["./plugins/**"],
|
|
70
|
+
packages: []
|
|
71
|
+
}, resolvedPath);
|
|
72
|
+
return resolvedPath;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Add a plugin glob pattern to the config
|
|
76
|
+
*/
|
|
77
|
+
async function addPluginGlob(glob, configPath) {
|
|
78
|
+
const config = await loadConfig(configPath);
|
|
79
|
+
if (!config.plugins) config.plugins = [];
|
|
80
|
+
if (config.plugins.includes(glob)) throw new Error(`Plugin pattern '${glob}' already exists in config`);
|
|
81
|
+
config.plugins.push(glob);
|
|
82
|
+
await saveConfig(config, configPath ?? await findConfigPath() ?? getDefaultConfigPath());
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Remove a plugin glob pattern from the config
|
|
86
|
+
*/
|
|
87
|
+
async function removePluginGlob(glob, configPath) {
|
|
88
|
+
const resolvedPath = configPath ?? await findConfigPath();
|
|
89
|
+
if (!resolvedPath) throw new Error("No config file found");
|
|
90
|
+
const config = await loadConfig(resolvedPath);
|
|
91
|
+
if (!config.plugins || !config.plugins.includes(glob)) throw new Error(`Plugin pattern '${glob}' not found in config`);
|
|
92
|
+
config.plugins = config.plugins.filter((p) => p !== glob);
|
|
93
|
+
await saveConfig(config, resolvedPath);
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* List all plugin glob patterns in the config
|
|
97
|
+
*/
|
|
98
|
+
async function listPluginGlobs(configPath) {
|
|
99
|
+
return (await loadConfig(configPath)).plugins ?? [];
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Get absolute plugin paths from config globs
|
|
103
|
+
*/
|
|
104
|
+
async function getPluginPaths(configPath) {
|
|
105
|
+
const resolvedConfigPath = configPath ?? await findConfigPath();
|
|
106
|
+
if (!resolvedConfigPath) return [];
|
|
107
|
+
const config = await loadConfig(resolvedConfigPath);
|
|
108
|
+
const configDir = path.dirname(resolvedConfigPath);
|
|
109
|
+
return (config.plugins ?? []).map((glob) => {
|
|
110
|
+
if (path.isAbsolute(glob)) return glob.replace(/\\/g, "/");
|
|
111
|
+
return path.resolve(configDir, glob).replace(/\\/g, "/");
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Add an npm package name to the config
|
|
116
|
+
*/
|
|
117
|
+
async function addPackage(packageName, configPath) {
|
|
118
|
+
const config = await loadConfig(configPath);
|
|
119
|
+
if (!config.packages) config.packages = [];
|
|
120
|
+
if (config.packages.includes(packageName)) throw new Error(`Package '${packageName}' already exists in config`);
|
|
121
|
+
config.packages.push(packageName);
|
|
122
|
+
await saveConfig(config, configPath ?? await findConfigPath() ?? getDefaultConfigPath());
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Remove an npm package name from the config
|
|
126
|
+
*/
|
|
127
|
+
async function removePackage(packageName, configPath) {
|
|
128
|
+
const resolvedPath = configPath ?? await findConfigPath();
|
|
129
|
+
if (!resolvedPath) throw new Error("No config file found");
|
|
130
|
+
const config = await loadConfig(resolvedPath);
|
|
131
|
+
if (!config.packages || !config.packages.includes(packageName)) throw new Error(`Package '${packageName}' not found in config`);
|
|
132
|
+
config.packages = config.packages.filter((p) => p !== packageName);
|
|
133
|
+
await saveConfig(config, resolvedPath);
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* List all npm package names in the config
|
|
137
|
+
*/
|
|
138
|
+
async function listPackages(configPath) {
|
|
139
|
+
return (await loadConfig(configPath)).packages ?? [];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
//#endregion
|
|
143
|
+
//#region src/utils/resolve-package.ts
|
|
144
|
+
const execFileAsync = promisify(execFile);
|
|
145
|
+
/**
|
|
146
|
+
* Resolve an npm package name to its installed root directory.
|
|
147
|
+
* Tries local node_modules first (relative to cwd), then global.
|
|
148
|
+
*/
|
|
149
|
+
async function resolvePackageDir(packageName) {
|
|
150
|
+
const localCandidate = path.join(process.cwd(), "node_modules", packageName);
|
|
151
|
+
try {
|
|
152
|
+
await fs.access(path.join(localCandidate, "package.json"));
|
|
153
|
+
return localCandidate;
|
|
154
|
+
} catch {}
|
|
155
|
+
const globalDir = await getGlobalNodeModulesDir();
|
|
156
|
+
if (globalDir) {
|
|
157
|
+
const globalCandidate = path.join(globalDir, packageName);
|
|
158
|
+
try {
|
|
159
|
+
await fs.access(path.join(globalCandidate, "package.json"));
|
|
160
|
+
return globalCandidate;
|
|
161
|
+
} catch {}
|
|
162
|
+
}
|
|
163
|
+
throw new Error(`Package '${packageName}' not found. Install it locally (npm install ${packageName}) or globally (npm install -g ${packageName}).`);
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Get the global node_modules directory using npm root -g.
|
|
167
|
+
*/
|
|
168
|
+
async function getGlobalNodeModulesDir() {
|
|
169
|
+
try {
|
|
170
|
+
const { stdout } = await execFileAsync("npm", ["root", "-g"], { encoding: "utf-8" });
|
|
171
|
+
const dir = stdout.trim();
|
|
172
|
+
await fs.access(dir);
|
|
173
|
+
return dir;
|
|
174
|
+
} catch {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
//#endregion
|
|
180
|
+
//#region src/commands/register-config.command.ts
|
|
181
|
+
async function registerConfigCommand(program) {
|
|
182
|
+
const configCommand = program.command("config").description("Manage lb-scaffold configuration");
|
|
183
|
+
configCommand.command("init").description("Initialize a new .lbscaffold config file in the current directory").action(async () => {
|
|
184
|
+
try {
|
|
185
|
+
const configPath = await initConfig();
|
|
186
|
+
console.log(`Created config file: ${configPath}`);
|
|
187
|
+
} catch (error) {
|
|
188
|
+
console.error(error.message);
|
|
189
|
+
process.exit(1);
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
configCommand.command("add <glob>").description("Add a plugin glob pattern to the config").action(async (glob) => {
|
|
193
|
+
try {
|
|
194
|
+
await addPluginGlob(glob);
|
|
195
|
+
console.log(`Added plugin pattern: ${glob}`);
|
|
196
|
+
} catch (error) {
|
|
197
|
+
console.error(error.message);
|
|
198
|
+
process.exit(1);
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
configCommand.command("remove <glob>").description("Remove a plugin glob pattern from the config").action(async (glob) => {
|
|
202
|
+
try {
|
|
203
|
+
await removePluginGlob(glob);
|
|
204
|
+
console.log(`Removed plugin pattern: ${glob}`);
|
|
205
|
+
} catch (error) {
|
|
206
|
+
console.error(error.message);
|
|
207
|
+
process.exit(1);
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
configCommand.command("list").description("List all plugin patterns and packages in the config").action(async () => {
|
|
211
|
+
const configPath = await findConfigPath();
|
|
212
|
+
if (!configPath) {
|
|
213
|
+
console.log("No .lbscaffold config file found.");
|
|
214
|
+
console.log("Run \"lb-scaffold config init\" to create one.");
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
console.log(`Config file: ${configPath}\n`);
|
|
218
|
+
const globs = await listPluginGlobs();
|
|
219
|
+
if (globs.length === 0) console.log("No plugin patterns configured.");
|
|
220
|
+
else {
|
|
221
|
+
console.log("Plugin patterns:");
|
|
222
|
+
for (const glob of globs) console.log(` - ${glob}`);
|
|
223
|
+
}
|
|
224
|
+
const packages = await listPackages();
|
|
225
|
+
if (packages.length > 0) {
|
|
226
|
+
console.log("\nPackages:");
|
|
227
|
+
for (const pkg of packages) console.log(` - ${pkg}`);
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
configCommand.command("show").description("Show the full config file contents").action(async () => {
|
|
231
|
+
const configPath = await findConfigPath();
|
|
232
|
+
if (!configPath) {
|
|
233
|
+
console.log("No .lbscaffold config file found.");
|
|
234
|
+
console.log("Run \"lb-scaffold config init\" to create one.");
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
console.log(`Config file: ${configPath}\n`);
|
|
238
|
+
const config = await loadConfig(configPath);
|
|
239
|
+
console.log(JSON.stringify(config, null, 2));
|
|
240
|
+
});
|
|
241
|
+
configCommand.command("add-package <name>").description("Add an npm package as a plugin source").action(async (name) => {
|
|
242
|
+
try {
|
|
243
|
+
await resolvePackageDir(name);
|
|
244
|
+
await addPackage(name);
|
|
245
|
+
console.log(`Added package: ${name}`);
|
|
246
|
+
} catch (error) {
|
|
247
|
+
console.error(error.message);
|
|
248
|
+
process.exit(1);
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
configCommand.command("remove-package <name>").description("Remove an npm package from plugin sources").action(async (name) => {
|
|
252
|
+
try {
|
|
253
|
+
await removePackage(name);
|
|
254
|
+
console.log(`Removed package: ${name}`);
|
|
255
|
+
} catch (error) {
|
|
256
|
+
console.error(error.message);
|
|
257
|
+
process.exit(1);
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
configCommand.command("list-packages").description("List all npm packages configured as plugin sources").action(async () => {
|
|
261
|
+
if (!await findConfigPath()) {
|
|
262
|
+
console.log("No .lbscaffold config file found.");
|
|
263
|
+
console.log("Run \"lb-scaffold config init\" to create one.");
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
const packages = await listPackages();
|
|
267
|
+
if (packages.length === 0) console.log("No packages configured.");
|
|
268
|
+
else {
|
|
269
|
+
console.log("Packages:");
|
|
270
|
+
for (const pkg of packages) console.log(` - ${pkg}`);
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
return configCommand;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
//#endregion
|
|
277
|
+
//#region src/commands/register-new.command.ts
|
|
278
|
+
let rl = null;
|
|
279
|
+
function getRL() {
|
|
280
|
+
if (!rl) rl = readline.createInterface({
|
|
281
|
+
input: process.stdin,
|
|
282
|
+
output: process.stdout
|
|
283
|
+
});
|
|
284
|
+
return rl;
|
|
285
|
+
}
|
|
286
|
+
function closeRL() {
|
|
287
|
+
rl?.close();
|
|
288
|
+
rl = null;
|
|
289
|
+
}
|
|
290
|
+
function ask(question) {
|
|
291
|
+
return new Promise((resolve) => getRL().question(question, resolve));
|
|
292
|
+
}
|
|
293
|
+
async function promptSelect(message, choices, defaultValue) {
|
|
294
|
+
const items = choices.map(String);
|
|
295
|
+
console.log(`\n ${message}`);
|
|
296
|
+
items.forEach((c, i) => {
|
|
297
|
+
const marker = c === String(defaultValue) ? " (default)" : "";
|
|
298
|
+
console.log(` ${i + 1}) ${c}${marker}`);
|
|
299
|
+
});
|
|
300
|
+
const answer = await ask(` Choice [${defaultValue ?? items[0]}]: `);
|
|
301
|
+
if (!answer.trim()) return String(defaultValue ?? items[0]);
|
|
302
|
+
const idx = parseInt(answer, 10);
|
|
303
|
+
if (idx >= 1 && idx <= items.length) return items[idx - 1];
|
|
304
|
+
if (items.includes(answer.trim())) return answer.trim();
|
|
305
|
+
return String(defaultValue ?? items[0]);
|
|
306
|
+
}
|
|
307
|
+
async function promptConfirm(message, defaultValue) {
|
|
308
|
+
const answer = await ask(` ${message} (Y/n) [${defaultValue === false ? "N" : "Y"}]: `);
|
|
309
|
+
if (!answer.trim()) return defaultValue !== false;
|
|
310
|
+
return answer.trim().toLowerCase().startsWith("y");
|
|
311
|
+
}
|
|
312
|
+
async function promptInput(message, defaultValue) {
|
|
313
|
+
return (await ask(` ${message}${defaultValue !== void 0 ? ` [${defaultValue}]` : ""}: `)).trim() || String(defaultValue ?? "");
|
|
314
|
+
}
|
|
315
|
+
function isBooleanFlag(opt) {
|
|
316
|
+
return !opt.flags.includes("<") && !opt.choices?.length;
|
|
317
|
+
}
|
|
318
|
+
async function promptForOption(key, def) {
|
|
319
|
+
if (def.choices?.length) return promptSelect(def.description, def.choices, def.defaultValue);
|
|
320
|
+
if (isBooleanFlag(def)) return promptConfirm(def.description, def.defaultValue);
|
|
321
|
+
return promptInput(def.description, def.defaultValue);
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Resolves plugin options iteratively:
|
|
325
|
+
* 1. Call getOptions(collected) to get currently-relevant options
|
|
326
|
+
* 2. Prompt for any unanswered options (or use CLI-provided values)
|
|
327
|
+
* 3. If new options appeared, repeat — the plugin may reveal more
|
|
328
|
+
* options based on the newly-collected answers
|
|
329
|
+
* 4. Stop when no new unanswered options appear
|
|
330
|
+
*/
|
|
331
|
+
async function resolveOptions(plugin, collected, cliOpts) {
|
|
332
|
+
while (true) {
|
|
333
|
+
const optionDefs = await plugin.getOptions(collected);
|
|
334
|
+
let hasNewOptions = false;
|
|
335
|
+
for (const [key, def] of Object.entries(optionDefs)) {
|
|
336
|
+
if (def === void 0) continue;
|
|
337
|
+
if (collected[key] !== void 0) continue;
|
|
338
|
+
hasNewOptions = true;
|
|
339
|
+
if (cliOpts[key] !== void 0) collected[key] = cliOpts[key];
|
|
340
|
+
else collected[key] = await promptForOption(key, def);
|
|
341
|
+
}
|
|
342
|
+
if (!hasNewOptions) break;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Pre-parse process.argv to extract values for a set of option definitions.
|
|
347
|
+
* This is a lightweight scan — no full Commander parse — so we can call
|
|
348
|
+
* getOptions() with the values before Commander registers anything.
|
|
349
|
+
*/
|
|
350
|
+
function preParseArgv(optionDefs) {
|
|
351
|
+
const args = process.argv.slice(2);
|
|
352
|
+
const result = {};
|
|
353
|
+
for (const [key, def] of Object.entries(optionDefs)) {
|
|
354
|
+
if (def === void 0) continue;
|
|
355
|
+
const flagMatch = def.flags.match(/--([a-z][a-z-]*)/i);
|
|
356
|
+
if (!flagMatch) continue;
|
|
357
|
+
const flagName = `--${flagMatch[1]}`;
|
|
358
|
+
const eqArg = args.find((a) => a.startsWith(`${flagName}=`));
|
|
359
|
+
if (eqArg) {
|
|
360
|
+
result[key] = eqArg.split("=")[1];
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
const idx = args.indexOf(flagName);
|
|
364
|
+
if (idx !== -1 && idx + 1 < args.length) {
|
|
365
|
+
if (!def.flags.includes("<")) continue;
|
|
366
|
+
result[key] = args[idx + 1];
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return result;
|
|
370
|
+
}
|
|
371
|
+
function registerOption(cmd, def) {
|
|
372
|
+
const cmdOption = new Option(def.flags, def.description);
|
|
373
|
+
if (def.defaultValue !== void 0) cmdOption.default(def.defaultValue);
|
|
374
|
+
if (def.choices?.length) cmdOption.choices(def.choices.map(String));
|
|
375
|
+
if (def.required) cmdOption.makeOptionMandatory();
|
|
376
|
+
cmd.addOption(cmdOption);
|
|
377
|
+
}
|
|
378
|
+
async function registerNewCommand(program, pluginManager) {
|
|
379
|
+
const newCmd = program.command("new").description("Create a new project from a template");
|
|
380
|
+
const templates = pluginManager.getPluginsByType(SCAFFOLD_TEMPLATE_PLUGIN_TYPE);
|
|
381
|
+
for (const meta of templates) {
|
|
382
|
+
const plugin = pluginManager.getPlugin(meta.id);
|
|
383
|
+
const sub = newCmd.command(plugin.argument).description(`Create a new ${plugin.argument} project`).argument("<name>", "Project name").option("--dry-run", "Simulate without writing files", false).option("--force", "Overwrite existing files", false).allowUnknownOption(true);
|
|
384
|
+
let optionDefs = await plugin.getOptions({ name: "" });
|
|
385
|
+
let preCollected = { name: "" };
|
|
386
|
+
let previousKeys = /* @__PURE__ */ new Set();
|
|
387
|
+
while (true) {
|
|
388
|
+
const currentKeys = new Set(Object.keys(optionDefs));
|
|
389
|
+
if ([...currentKeys].filter((k) => !previousKeys.has(k)).length === 0) break;
|
|
390
|
+
const parsed = preParseArgv(optionDefs);
|
|
391
|
+
preCollected = {
|
|
392
|
+
...preCollected,
|
|
393
|
+
...parsed
|
|
394
|
+
};
|
|
395
|
+
previousKeys = currentKeys;
|
|
396
|
+
optionDefs = await plugin.getOptions(preCollected);
|
|
397
|
+
}
|
|
398
|
+
for (const [, def] of Object.entries(optionDefs)) {
|
|
399
|
+
if (def === void 0) continue;
|
|
400
|
+
registerOption(sub, def);
|
|
401
|
+
}
|
|
402
|
+
sub.action(async (name, cliOpts) => {
|
|
403
|
+
const collected = {
|
|
404
|
+
name,
|
|
405
|
+
dryRun: cliOpts.dryRun,
|
|
406
|
+
force: cliOpts.force
|
|
407
|
+
};
|
|
408
|
+
await resolveOptions(plugin, collected, cliOpts);
|
|
409
|
+
closeRL();
|
|
410
|
+
await plugin.execute(collected);
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
return newCmd;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
//#endregion
|
|
417
|
+
//#region src/cli.ts
|
|
418
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
419
|
+
const PLUGINS_FOLDER = path.resolve(__dirname, "../templates").replace(/\\/g, "/");
|
|
420
|
+
const userPluginPaths = await getPluginPaths();
|
|
421
|
+
const packageNames = await listPackages();
|
|
422
|
+
const packagePaths = await Promise.all(packageNames.map(async (name) => {
|
|
423
|
+
return await resolvePackageDir(name);
|
|
424
|
+
}));
|
|
425
|
+
const pluginManager = new PluginManager();
|
|
426
|
+
await pluginManager.loadPlugins([
|
|
427
|
+
PLUGINS_FOLDER,
|
|
428
|
+
...userPluginPaths,
|
|
429
|
+
...packagePaths
|
|
430
|
+
]);
|
|
431
|
+
const program = new InteractiveCommand();
|
|
432
|
+
program.name("lb-scaffold").description("Scaffold new projects from templates");
|
|
433
|
+
await registerNewCommand(program, pluginManager);
|
|
434
|
+
await registerConfigCommand(program);
|
|
435
|
+
program.addOption(new Option("-i, --interactive", "Run in interactive mode").default(true));
|
|
436
|
+
await program.interactive().parseAsync();
|
|
437
|
+
|
|
438
|
+
//#endregion
|
|
439
|
+
export { };
|
|
4
440
|
//# sourceMappingURL=cli.mjs.map
|