@dawitworku/projectcli 0.2.2 → 3.0.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/src/index.js CHANGED
@@ -28,15 +28,20 @@ try {
28
28
  const { getLanguages, getFrameworks, getGenerator } = require("./registry");
29
29
  const { runSteps } = require("./run");
30
30
  const { runAdd } = require("./add");
31
- const { checkBinaries } = require("./preflight");
31
+ const { checkBinaries, getInstallHint } = require("./preflight");
32
32
  const { gitClone, removeGitFolder } = require("./remote");
33
33
  const { generateCI, generateDocker } = require("./cicd");
34
34
  const { generateDevContainer } = require("./devcontainer");
35
35
  const { generateLicense, licenseTypes } = require("./license");
36
36
  const { getDescription } = require("./descriptions");
37
37
  const { detectLanguage, detectPackageManager } = require("./detect");
38
- const { loadConfig } = require("./config");
38
+ const { loadConfig, loadProjectConfig } = require("./config");
39
39
  const { runConfig } = require("./settings");
40
+ const { runDoctor } = require("./core/doctor");
41
+ const { runPreset } = require("./preset");
42
+ const { getPreset } = require("./presets");
43
+ const { runUpgrade } = require("./upgrade");
44
+ const { runPlugin } = require("./plugin");
40
45
 
41
46
  const RUST_KEYWORDS = new Set(
42
47
  [
@@ -197,7 +202,15 @@ function splitCommand(argv) {
197
202
  if (!argv || argv.length === 0) return { cmd: "init", rest: [] };
198
203
  const first = argv[0];
199
204
  if (typeof first === "string" && !first.startsWith("-")) {
200
- if (first === "init" || first === "add") {
205
+ if (
206
+ first === "init" ||
207
+ first === "add" ||
208
+ first === "config" ||
209
+ first === "doctor" ||
210
+ first === "upgrade" ||
211
+ first === "preset" ||
212
+ first === "plugin"
213
+ ) {
201
214
  return { cmd: first, rest: argv.slice(1) };
202
215
  }
203
216
  }
@@ -211,11 +224,22 @@ function printHelp() {
211
224
  console.log(" projectcli # init a new project");
212
225
  console.log(" projectcli init # init a new project");
213
226
  console.log(" projectcli add # add libraries to current project");
227
+ console.log(" projectcli add ci # add GitHub Actions CI");
228
+ console.log(" projectcli add docker # add Dockerfile");
229
+ console.log(" projectcli add devcontainer # add VS Code devcontainer");
230
+ console.log(" projectcli add license # add LICENSE");
231
+ console.log(" projectcli add lint # add linter defaults");
232
+ console.log(" projectcli add test # add test runner defaults");
233
+ console.log(" projectcli doctor # check a repo and optionally fix");
234
+ console.log(" projectcli preset # manage presets");
235
+ console.log(" projectcli upgrade # upgrade configs safely");
236
+ console.log(" projectcli plugin # manage plugins");
214
237
  console.log(" projectcli --list # list all frameworks");
215
238
  console.log(
216
239
  " projectcli --language <lang> --framework <fw> --name <project>"
217
240
  );
218
241
  console.log(" projectcli config # configure defaults");
242
+ console.log(" # config files: .projectclirc or projectcli.config.json");
219
243
  console.log("");
220
244
  console.log("Flags:");
221
245
  console.log(" --help, -h Show help");
@@ -236,6 +260,23 @@ function printHelp() {
236
260
  console.log(" --no-license Never add LICENSE");
237
261
  console.log(" --learning Enable learning mode (shows descriptions)");
238
262
  console.log(" --template Clone from a Git repository URL");
263
+ console.log("");
264
+ console.log("Doctor flags:");
265
+ console.log(" --fix Apply safe fixes");
266
+ console.log(" --json JSON output (CI-friendly)");
267
+ console.log(" --ci-only Only check CI");
268
+
269
+ console.log("");
270
+ console.log("Upgrade flags:");
271
+ console.log(" --preview Show what would change");
272
+ console.log(" --only ci Upgrade only CI templates");
273
+ console.log(" --only docker Upgrade Docker templates");
274
+ console.log(" --only devcontainer Upgrade devcontainer templates");
275
+
276
+ console.log("");
277
+ console.log("Plugin commands:");
278
+ console.log(" projectcli plugin list");
279
+ console.log(" projectcli plugin install <id>");
239
280
  }
240
281
 
241
282
  const BACK = "__back__";
@@ -287,17 +328,31 @@ function filterExistingWriteFiles(steps, projectRoot) {
287
328
  return { kept, skipped };
288
329
  }
289
330
 
290
- function printList() {
291
- for (const lang of getLanguages()) {
331
+ function printList(effectiveConfig) {
332
+ for (const lang of getLanguages(effectiveConfig)) {
292
333
  console.log(`${lang}:`);
293
- for (const fw of getFrameworks(lang)) {
294
- const gen = getGenerator(lang, fw);
334
+ for (const fw of getFrameworks(lang, effectiveConfig)) {
335
+ const gen = getGenerator(lang, fw, effectiveConfig);
295
336
  const note = gen?.notes ? ` - ${gen.notes}` : "";
296
- console.log(` - ${fw}${note}`);
337
+ const stability = gen?.stability ? ` [${gen.stability}]` : "";
338
+ console.log(` - ${fw}${stability}${note}`);
297
339
  }
298
340
  }
299
341
  }
300
342
 
343
+ function formatStabilityBadge(stability) {
344
+ if (stability === "core") return chalk.green("✅ Core");
345
+ if (stability === "experimental") return chalk.yellow("⚠ Experimental");
346
+ if (stability === "community") return chalk.cyan("👥 Community");
347
+ return null;
348
+ }
349
+
350
+ function printMissingTool(bin) {
351
+ console.log(chalk.red(`✖ ${bin} not found`));
352
+ const hint = getInstallHint(bin);
353
+ if (hint) console.log(chalk.dim(` → ${hint}`));
354
+ }
355
+
301
356
  function showBanner() {
302
357
  console.log("");
303
358
  console.log(
@@ -340,13 +395,20 @@ async function main(options = {}) {
340
395
  console.log(readPackageVersion());
341
396
  return;
342
397
  }
398
+
399
+ // Config precedence: CLI args > project config > global config (~/.projectcli.json)
400
+ const userConfig = loadConfig();
401
+ const projectConfigInfo = loadProjectConfig(process.cwd());
402
+ const projectConfig = projectConfigInfo.data || {};
403
+ const effectiveConfig = { ...userConfig, ...projectConfig };
404
+
343
405
  if (args.list) {
344
- printList();
406
+ printList(effectiveConfig);
345
407
  return;
346
408
  }
347
409
 
348
410
  if (cmd === "add") {
349
- await runAdd({ prompt, argv: rest });
411
+ await runAdd({ prompt, argv: rest, effectiveConfig });
350
412
  return;
351
413
  }
352
414
 
@@ -355,6 +417,26 @@ async function main(options = {}) {
355
417
  return;
356
418
  }
357
419
 
420
+ if (cmd === "doctor") {
421
+ await runDoctor({ prompt, argv: rest, effectiveConfig });
422
+ return;
423
+ }
424
+
425
+ if (cmd === "preset") {
426
+ await runPreset({ prompt, argv: rest, effectiveConfig });
427
+ return;
428
+ }
429
+
430
+ if (cmd === "upgrade") {
431
+ await runUpgrade({ prompt, argv: rest });
432
+ return;
433
+ }
434
+
435
+ if (cmd === "plugin") {
436
+ await runPlugin({ prompt, argv: rest, effectiveConfig });
437
+ return;
438
+ }
439
+
358
440
  // Smart Context Detection
359
441
  if (
360
442
  cmd === "init" &&
@@ -452,7 +534,7 @@ async function main(options = {}) {
452
534
  type: "input",
453
535
  name: "author",
454
536
  message: "Author Name:",
455
- default: userConfig.author || "The Authors",
537
+ default: effectiveConfig.author || "The Authors",
456
538
  },
457
539
  ]);
458
540
  const steps = generateLicense(process.cwd(), type, author);
@@ -495,40 +577,80 @@ async function main(options = {}) {
495
577
  chalk.dim(" (Type to search)")
496
578
  );
497
579
 
498
- const languages = getLanguages();
580
+ const languages = getLanguages(effectiveConfig);
499
581
  if (languages.length === 0) {
500
582
  throw new Error("No languages configured.");
501
583
  }
502
584
 
503
- const userConfig = loadConfig();
504
-
505
585
  const allowedPms = ["npm", "pnpm", "yarn", "bun"];
506
586
  let preselectedPm =
507
587
  typeof args.pm === "string" && allowedPms.includes(args.pm)
508
588
  ? args.pm
509
589
  : undefined;
510
590
 
511
- if (!preselectedPm && userConfig.packageManager) {
512
- if (allowedPms.includes(userConfig.packageManager)) {
513
- preselectedPm = userConfig.packageManager;
591
+ const configuredPm =
592
+ typeof effectiveConfig.packageManager === "string"
593
+ ? effectiveConfig.packageManager
594
+ : undefined;
595
+
596
+ if (!preselectedPm && configuredPm) {
597
+ if (allowedPms.includes(configuredPm)) {
598
+ preselectedPm = configuredPm;
514
599
  }
515
600
  }
516
601
 
517
- if (userConfig.learningMode && args.learning === false) {
602
+ const presetId =
603
+ typeof effectiveConfig.preset === "string" && effectiveConfig.preset.trim()
604
+ ? effectiveConfig.preset.trim()
605
+ : "startup";
606
+ const preset = getPreset(presetId, effectiveConfig);
607
+ const presetPm =
608
+ typeof preset?.defaults?.packageManager === "string"
609
+ ? preset.defaults.packageManager
610
+ : null;
611
+
612
+ if (!preselectedPm && presetPm && allowedPms.includes(presetPm)) {
613
+ preselectedPm = presetPm;
614
+ }
615
+
616
+ const learningEnabled =
617
+ typeof effectiveConfig.learningMode === "boolean"
618
+ ? effectiveConfig.learningMode
619
+ : typeof effectiveConfig.learning === "boolean"
620
+ ? effectiveConfig.learning
621
+ : false;
622
+
623
+ if (learningEnabled && args.learning === false) {
518
624
  args.learning = true;
519
625
  }
520
626
 
521
627
  const defaultLicenseType =
522
- typeof userConfig.defaultLicense === "string" &&
523
- licenseTypes.includes(userConfig.defaultLicense)
524
- ? userConfig.defaultLicense
628
+ typeof effectiveConfig.license === "string" &&
629
+ licenseTypes.includes(effectiveConfig.license)
630
+ ? effectiveConfig.license
631
+ : typeof effectiveConfig.defaultLicense === "string" &&
632
+ licenseTypes.includes(effectiveConfig.defaultLicense)
633
+ ? effectiveConfig.defaultLicense
525
634
  : null;
526
635
 
527
636
  const defaultAuthor =
528
- typeof userConfig.author === "string" && userConfig.author.trim()
529
- ? userConfig.author.trim()
637
+ typeof effectiveConfig.author === "string" && effectiveConfig.author.trim()
638
+ ? effectiveConfig.author.trim()
530
639
  : "The Authors";
531
640
 
641
+ const defaultCi =
642
+ typeof effectiveConfig.ci === "boolean"
643
+ ? effectiveConfig.ci
644
+ : preset?.defaults?.extras?.ci === true;
645
+ const defaultDocker =
646
+ typeof effectiveConfig.docker === "boolean"
647
+ ? effectiveConfig.docker
648
+ : preset?.defaults?.extras?.docker === true;
649
+ const defaultDevContainer =
650
+ typeof effectiveConfig.devcontainer === "boolean"
651
+ ? effectiveConfig.devcontainer
652
+ : preset?.defaults?.extras?.devcontainer === true;
653
+
532
654
  const state = {
533
655
  template: args.template || undefined,
534
656
  language:
@@ -541,7 +663,10 @@ async function main(options = {}) {
541
663
  };
542
664
 
543
665
  if (state.language) {
544
- const frameworksForLanguage = getFrameworks(state.language);
666
+ const frameworksForLanguage = getFrameworks(
667
+ state.language,
668
+ effectiveConfig
669
+ );
545
670
  state.framework =
546
671
  args.framework && frameworksForLanguage.includes(args.framework)
547
672
  ? args.framework
@@ -564,7 +689,7 @@ async function main(options = {}) {
564
689
  while (true) {
565
690
  if (step === "language") {
566
691
  const languageChoices = languages.map((lang) => {
567
- const count = getFrameworks(lang).length;
692
+ const count = getFrameworks(lang, effectiveConfig).length;
568
693
  return { name: `${lang} (${count})`, value: lang, short: lang };
569
694
  });
570
695
 
@@ -603,15 +728,19 @@ async function main(options = {}) {
603
728
  }
604
729
 
605
730
  if (step === "framework") {
606
- const frameworks = getFrameworks(state.language);
731
+ const frameworks = getFrameworks(state.language, effectiveConfig);
607
732
  if (frameworks.length === 0) {
608
733
  throw new Error(`No frameworks configured for ${state.language}.`);
609
734
  }
610
735
 
611
736
  const frameworkChoices = frameworks.map((fw) => {
612
- const gen = getGenerator(state.language, fw);
737
+ const gen = getGenerator(state.language, fw, effectiveConfig);
613
738
  const note = gen?.notes ? ` — ${gen.notes}` : "";
614
- return { name: `${fw}${note}`, value: fw, short: fw };
739
+ const badge = gen?.stability
740
+ ? formatStabilityBadge(gen.stability)
741
+ : null;
742
+ const badgeText = badge ? ` ${badge}` : "";
743
+ return { name: `${fw}${badgeText}${note}`, value: fw, short: fw };
615
744
  });
616
745
 
617
746
  const frameworkQuestion = hasAutocomplete
@@ -650,7 +779,11 @@ async function main(options = {}) {
650
779
  state.framework = answer.framework;
651
780
 
652
781
  // Preflight Checks
653
- const gen = getGenerator(state.language, state.framework);
782
+ const gen = getGenerator(
783
+ state.language,
784
+ state.framework,
785
+ effectiveConfig
786
+ );
654
787
  if (gen && gen.check && gen.check.length > 0) {
655
788
  /* eslint-disable-next-line no-console */
656
789
  console.log(chalk.dim("\n(checking requirements...)"));
@@ -659,7 +792,7 @@ async function main(options = {}) {
659
792
 
660
793
  if (missing.length > 0) {
661
794
  console.log(chalk.red.bold("\nMissing required tools:"));
662
- missing.forEach((m) => console.log(chalk.red(` - ${m.bin}`)));
795
+ missing.forEach((m) => printMissingTool(m.bin));
663
796
  console.log(
664
797
  chalk.yellow("You may not be able to build or run this project.\n")
665
798
  );
@@ -830,9 +963,10 @@ async function main(options = {}) {
830
963
  langArg = "JavaScript";
831
964
  if (detectedTemplate === "Java/Kotlin") langArg = "Java";
832
965
 
833
- let wantCi = Boolean(args.ci);
834
- let wantDocker = Boolean(args.docker);
835
- let wantDevContainer = Boolean(args.devcontainer);
966
+ let wantCi = Boolean(args.ci) || defaultCi;
967
+ let wantDocker = Boolean(args.docker) || defaultDocker;
968
+ let wantDevContainer =
969
+ Boolean(args.devcontainer) || defaultDevContainer;
836
970
  let wantLicense =
837
971
  typeof args.license === "boolean"
838
972
  ? args.license
@@ -928,7 +1062,14 @@ async function main(options = {}) {
928
1062
  )
929
1063
  );
930
1064
  } catch (err) {
931
- console.error(chalk.red("\nFailed to clone template:"), err.message);
1065
+ const message = err && err.message ? err.message : String(err);
1066
+ console.error(chalk.red("\nFailed to clone template:"), message);
1067
+ if (
1068
+ /\bENOENT\b/i.test(message) ||
1069
+ /command not found/i.test(message)
1070
+ ) {
1071
+ printMissingTool("git");
1072
+ }
932
1073
  process.exit(1);
933
1074
  }
934
1075
  return;
@@ -1020,6 +1161,11 @@ async function main(options = {}) {
1020
1161
  const message = err && err.message ? err.message : String(err);
1021
1162
  console.error(`\nError: ${message}`);
1022
1163
 
1164
+ const cmdNotFound = /^Command not found:\s*(.+)$/.exec(message);
1165
+ if (cmdNotFound && cmdNotFound[1]) {
1166
+ printMissingTool(cmdNotFound[1].trim());
1167
+ }
1168
+
1023
1169
  const looksLikeNameIssue =
1024
1170
  /cannot be used as a package name|Rust keyword|keyword|Cargo\.toml/i.test(
1025
1171
  message
@@ -1105,9 +1251,10 @@ async function main(options = {}) {
1105
1251
 
1106
1252
  // CI/CD & Docker & DevContainer
1107
1253
  if (!args.dryRun) {
1108
- let wantCi = args.ci;
1109
- let wantDocker = args.docker;
1110
- let wantDevContainer = Boolean(args.devcontainer);
1254
+ let wantCi = Boolean(args.ci) || defaultCi;
1255
+ let wantDocker = Boolean(args.docker) || defaultDocker;
1256
+ let wantDevContainer =
1257
+ Boolean(args.devcontainer) || defaultDevContainer;
1111
1258
  let wantLicense =
1112
1259
  typeof args.license === "boolean"
1113
1260
  ? args.license
package/src/plugin.js ADDED
@@ -0,0 +1,140 @@
1
+ const chalk = require("chalk");
2
+
3
+ const { loadConfig, saveConfig, CONFIG_PATH } = require("./config");
4
+ const { getEnabledPlugins, tryLoadPlugin } = require("./plugins/manager");
5
+
6
+ function parsePluginArgs(argv) {
7
+ const out = { yes: false, dryRun: false };
8
+ const args = Array.isArray(argv) ? argv : [];
9
+ for (const a of args) {
10
+ if (a === "--yes" || a === "-y") out.yes = true;
11
+ else if (a === "--dry-run") out.dryRun = true;
12
+ }
13
+ return out;
14
+ }
15
+
16
+ function firstPositional(argv) {
17
+ const args = Array.isArray(argv) ? argv : [];
18
+ for (const a of args) {
19
+ if (typeof a !== "string") continue;
20
+ if (a.startsWith("-")) continue;
21
+ return a;
22
+ }
23
+ return null;
24
+ }
25
+
26
+ function secondPositional(argv) {
27
+ const args = Array.isArray(argv) ? argv : [];
28
+ let seenFirst = false;
29
+ for (const a of args) {
30
+ if (typeof a !== "string") continue;
31
+ if (a.startsWith("-")) continue;
32
+ if (!seenFirst) {
33
+ seenFirst = true;
34
+ continue;
35
+ }
36
+ return a;
37
+ }
38
+ return null;
39
+ }
40
+
41
+ function printPluginList(effectiveConfig) {
42
+ const enabled = getEnabledPlugins(effectiveConfig);
43
+
44
+ console.log(chalk.bold("\nPlugins:"));
45
+ if (enabled.length === 0) {
46
+ console.log(chalk.dim("(none enabled)"));
47
+ console.log(chalk.dim(`Config: ${CONFIG_PATH}`));
48
+ console.log("");
49
+ return;
50
+ }
51
+
52
+ for (const id of enabled) {
53
+ const loaded = tryLoadPlugin(id);
54
+ if (loaded.ok) {
55
+ console.log(`${chalk.green("✓")} ${id}`);
56
+ } else {
57
+ console.log(`${chalk.red("✗")} ${id}`);
58
+ console.log(chalk.dim(` → ${loaded.error}`));
59
+ }
60
+ }
61
+ console.log(chalk.dim(`\nConfig: ${CONFIG_PATH}`));
62
+ console.log("");
63
+ }
64
+
65
+ async function runPlugin({ prompt, argv, effectiveConfig }) {
66
+ const flags = parsePluginArgs(argv);
67
+ const args = Array.isArray(argv) ? argv : [];
68
+
69
+ const sub = firstPositional(args);
70
+
71
+ if (!sub || sub === "list") {
72
+ printPluginList(effectiveConfig);
73
+ return;
74
+ }
75
+
76
+ if (sub === "install") {
77
+ const pluginId = secondPositional(args);
78
+ if (!pluginId) {
79
+ throw new Error(
80
+ "Missing plugin id. Example: projectcli plugin install my-company"
81
+ );
82
+ }
83
+
84
+ const cfg = loadConfig();
85
+ const current = Array.isArray(cfg.plugins) ? cfg.plugins : [];
86
+ const enabled = new Set(
87
+ current.map((p) => String(p || "").trim()).filter(Boolean)
88
+ );
89
+
90
+ if (enabled.has(pluginId)) {
91
+ console.log(chalk.dim(`\nAlready enabled: ${pluginId}`));
92
+ return;
93
+ }
94
+
95
+ const loaded = tryLoadPlugin(pluginId);
96
+ if (!loaded.ok) {
97
+ console.log(
98
+ chalk.yellow("\nNote: this command does not install packages.")
99
+ );
100
+ console.log(
101
+ chalk.yellow(
102
+ `It only enables a plugin that is already resolvable by Node: ${pluginId}`
103
+ )
104
+ );
105
+ }
106
+
107
+ if (flags.dryRun) {
108
+ console.log(chalk.bold("\nPlanned actions:"));
109
+ console.log(chalk.gray("- ") + chalk.yellow(`enable plugin ${pluginId}`));
110
+ console.log("\nDry run: nothing executed.");
111
+ return;
112
+ }
113
+
114
+ if (!flags.yes) {
115
+ const { ok } = await prompt([
116
+ {
117
+ type: "confirm",
118
+ name: "ok",
119
+ message: `Enable plugin ${pluginId} in ${CONFIG_PATH}?`,
120
+ default: true,
121
+ },
122
+ ]);
123
+ if (!ok) return;
124
+ }
125
+
126
+ enabled.add(pluginId);
127
+ saveConfig({ plugins: Array.from(enabled) });
128
+ console.log(chalk.green(`\n✓ Enabled plugin: ${pluginId}`));
129
+ console.log(chalk.dim(`Config: ${CONFIG_PATH}`));
130
+ return;
131
+ }
132
+
133
+ throw new Error(
134
+ `Unknown plugin subcommand: ${sub}. Try: projectcli plugin list | projectcli plugin install <id>`
135
+ );
136
+ }
137
+
138
+ module.exports = {
139
+ runPlugin,
140
+ };
@@ -0,0 +1,101 @@
1
+ const { getEnabledPlugins, tryLoadPlugin } = require("./manager");
2
+
3
+ function loadEnabledPlugins(effectiveConfig) {
4
+ const enabled = getEnabledPlugins(effectiveConfig);
5
+ const plugins = [];
6
+ const errors = [];
7
+
8
+ for (const id of enabled) {
9
+ const res = tryLoadPlugin(id);
10
+ if (res.ok) {
11
+ plugins.push({ id, module: res.module });
12
+ } else {
13
+ errors.push({ id, error: res.error || "Failed to load plugin" });
14
+ }
15
+ }
16
+
17
+ return { plugins, errors };
18
+ }
19
+
20
+ function getPluginPresets(effectiveConfig) {
21
+ const { plugins } = loadEnabledPlugins(effectiveConfig);
22
+ const out = [];
23
+
24
+ for (const p of plugins) {
25
+ const mod = p.module || {};
26
+ const presets = mod.presets;
27
+ if (!presets) continue;
28
+
29
+ if (Array.isArray(presets)) {
30
+ for (const preset of presets) {
31
+ if (preset && typeof preset.id === "string") {
32
+ out.push({ ...preset, pluginId: p.id });
33
+ }
34
+ }
35
+ continue;
36
+ }
37
+
38
+ if (typeof presets === "object") {
39
+ for (const key of Object.keys(presets)) {
40
+ const preset = presets[key];
41
+ if (!preset) continue;
42
+ const id = typeof preset.id === "string" ? preset.id : key;
43
+ if (!id) continue;
44
+ out.push({ ...preset, id, pluginId: p.id });
45
+ }
46
+ }
47
+ }
48
+
49
+ return out;
50
+ }
51
+
52
+ function getPluginRegistryAdditions(effectiveConfig) {
53
+ const { plugins } = loadEnabledPlugins(effectiveConfig);
54
+ const out = [];
55
+
56
+ for (const p of plugins) {
57
+ const mod = p.module || {};
58
+ const reg = mod.registry;
59
+ if (!reg) continue;
60
+
61
+ // Supported shapes:
62
+ // 1) registry: { "Language": { "Framework": generator, ... }, ... }
63
+ // 2) registry: { languages: { ... } }
64
+ const languages =
65
+ reg.languages && typeof reg.languages === "object" ? reg.languages : reg;
66
+ if (!languages || typeof languages !== "object") continue;
67
+
68
+ out.push({ pluginId: p.id, languages });
69
+ }
70
+
71
+ return out;
72
+ }
73
+
74
+ function getPluginDoctorChecks(effectiveConfig) {
75
+ const { plugins } = loadEnabledPlugins(effectiveConfig);
76
+ const out = [];
77
+
78
+ for (const p of plugins) {
79
+ const mod = p.module || {};
80
+
81
+ const checks = Array.isArray(mod.doctorChecks)
82
+ ? mod.doctorChecks
83
+ : Array.isArray(mod.doctor?.checks)
84
+ ? mod.doctor.checks
85
+ : [];
86
+
87
+ for (const check of checks) {
88
+ if (!check || typeof check.id !== "string") continue;
89
+ out.push({ ...check, pluginId: p.id });
90
+ }
91
+ }
92
+
93
+ return out;
94
+ }
95
+
96
+ module.exports = {
97
+ loadEnabledPlugins,
98
+ getPluginPresets,
99
+ getPluginRegistryAdditions,
100
+ getPluginDoctorChecks,
101
+ };
@@ -0,0 +1,38 @@
1
+ function uniqStrings(items) {
2
+ const out = [];
3
+ const seen = new Set();
4
+ for (const v of items || []) {
5
+ if (typeof v !== "string") continue;
6
+ const s = v.trim();
7
+ if (!s) continue;
8
+ if (seen.has(s)) continue;
9
+ seen.add(s);
10
+ out.push(s);
11
+ }
12
+ return out;
13
+ }
14
+
15
+ function getEnabledPlugins(effectiveConfig) {
16
+ const fromConfig = Array.isArray(effectiveConfig?.plugins)
17
+ ? effectiveConfig.plugins
18
+ : [];
19
+ return uniqStrings(fromConfig);
20
+ }
21
+
22
+ function tryLoadPlugin(pluginId) {
23
+ try {
24
+ // Plugins are expected to be resolvable via Node resolution.
25
+ // This command only enables/disables plugins in config; it does not install packages.
26
+ // eslint-disable-next-line global-require, import/no-dynamic-require
27
+ const mod = require(pluginId);
28
+ return { ok: true, module: mod, error: null };
29
+ } catch (err) {
30
+ const message = err && err.message ? err.message : String(err);
31
+ return { ok: false, module: null, error: message };
32
+ }
33
+ }
34
+
35
+ module.exports = {
36
+ getEnabledPlugins,
37
+ tryLoadPlugin,
38
+ };