@dawitworku/projectcli 0.2.1 → 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,13 +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
+ const { generateDevContainer } = require("./devcontainer");
35
+ const { generateLicense, licenseTypes } = require("./license");
34
36
  const { getDescription } = require("./descriptions");
35
37
  const { detectLanguage, detectPackageManager } = require("./detect");
36
- const { loadConfig } = require("./config");
38
+ const { loadConfig, loadProjectConfig } = require("./config");
37
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");
38
45
 
39
46
  const RUST_KEYWORDS = new Set(
40
47
  [
@@ -138,6 +145,8 @@ function parseArgs(argv) {
138
145
  pm: undefined,
139
146
  ci: false,
140
147
  docker: false,
148
+ devcontainer: false,
149
+ license: undefined,
141
150
  learning: false,
142
151
  template: undefined,
143
152
  };
@@ -156,6 +165,9 @@ function parseArgs(argv) {
156
165
  else if (a === "--dry-run") out.dryRun = true;
157
166
  else if (a === "--ci") out.ci = true;
158
167
  else if (a === "--docker") out.docker = true;
168
+ else if (a === "--devcontainer") out.devcontainer = true;
169
+ else if (a === "--license") out.license = true;
170
+ else if (a === "--no-license") out.license = false;
159
171
  else if (a === "--learning") out.learning = true;
160
172
  else if (a.startsWith("--template="))
161
173
  out.template = a.slice("--template=".length);
@@ -190,7 +202,15 @@ function splitCommand(argv) {
190
202
  if (!argv || argv.length === 0) return { cmd: "init", rest: [] };
191
203
  const first = argv[0];
192
204
  if (typeof first === "string" && !first.startsWith("-")) {
193
- 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
+ ) {
194
214
  return { cmd: first, rest: argv.slice(1) };
195
215
  }
196
216
  }
@@ -204,11 +224,22 @@ function printHelp() {
204
224
  console.log(" projectcli # init a new project");
205
225
  console.log(" projectcli init # init a new project");
206
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");
207
237
  console.log(" projectcli --list # list all frameworks");
208
238
  console.log(
209
239
  " projectcli --language <lang> --framework <fw> --name <project>"
210
240
  );
211
241
  console.log(" projectcli config # configure defaults");
242
+ console.log(" # config files: .projectclirc or projectcli.config.json");
212
243
  console.log("");
213
244
  console.log("Flags:");
214
245
  console.log(" --help, -h Show help");
@@ -224,8 +255,28 @@ function printHelp() {
224
255
  );
225
256
  console.log(" --ci Auto-add GitHub Actions CI");
226
257
  console.log(" --docker Auto-add Dockerfile");
258
+ console.log(" --devcontainer Auto-add VS Code Dev Container");
259
+ console.log(" --license Force-add LICENSE (uses config defaults)");
260
+ console.log(" --no-license Never add LICENSE");
227
261
  console.log(" --learning Enable learning mode (shows descriptions)");
228
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>");
229
280
  }
230
281
 
231
282
  const BACK = "__back__";
@@ -261,17 +312,47 @@ function printStepsPreview(steps) {
261
312
  console.log("");
262
313
  }
263
314
 
264
- function printList() {
265
- for (const lang of getLanguages()) {
315
+ function filterExistingWriteFiles(steps, projectRoot) {
316
+ const kept = [];
317
+ const skipped = [];
318
+ for (const step of steps || []) {
319
+ if (step && step.type === "writeFile" && typeof step.path === "string") {
320
+ const target = path.resolve(projectRoot, step.path);
321
+ if (fs.existsSync(target)) {
322
+ skipped.push(step.path);
323
+ continue;
324
+ }
325
+ }
326
+ kept.push(step);
327
+ }
328
+ return { kept, skipped };
329
+ }
330
+
331
+ function printList(effectiveConfig) {
332
+ for (const lang of getLanguages(effectiveConfig)) {
266
333
  console.log(`${lang}:`);
267
- for (const fw of getFrameworks(lang)) {
268
- const gen = getGenerator(lang, fw);
334
+ for (const fw of getFrameworks(lang, effectiveConfig)) {
335
+ const gen = getGenerator(lang, fw, effectiveConfig);
269
336
  const note = gen?.notes ? ` - ${gen.notes}` : "";
270
- console.log(` - ${fw}${note}`);
337
+ const stability = gen?.stability ? ` [${gen.stability}]` : "";
338
+ console.log(` - ${fw}${stability}${note}`);
271
339
  }
272
340
  }
273
341
  }
274
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
+
275
356
  function showBanner() {
276
357
  console.log("");
277
358
  console.log(
@@ -314,13 +395,20 @@ async function main(options = {}) {
314
395
  console.log(readPackageVersion());
315
396
  return;
316
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
+
317
405
  if (args.list) {
318
- printList();
406
+ printList(effectiveConfig);
319
407
  return;
320
408
  }
321
409
 
322
410
  if (cmd === "add") {
323
- await runAdd({ prompt, argv: rest });
411
+ await runAdd({ prompt, argv: rest, effectiveConfig });
324
412
  return;
325
413
  }
326
414
 
@@ -329,6 +417,26 @@ async function main(options = {}) {
329
417
  return;
330
418
  }
331
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
+
332
440
  // Smart Context Detection
333
441
  if (
334
442
  cmd === "init" &&
@@ -361,6 +469,8 @@ async function main(options = {}) {
361
469
  { name: "Add Library / Dependency", value: "add" },
362
470
  { name: "Add GitHub Actions CI", value: "ci" },
363
471
  { name: "Add Dockerfile", value: "docker" },
472
+ { name: "Add Dev Container (VS Code)", value: "devcontainer" },
473
+ { name: "Add License", value: "license" },
364
474
  new inquirer.Separator(),
365
475
  { name: "Start New Project Here", value: "new" },
366
476
  { name: "Exit", value: "exit" },
@@ -376,20 +486,31 @@ async function main(options = {}) {
376
486
  return;
377
487
  }
378
488
 
379
- if (action === "ci" || action === "docker") {
489
+ if (action === "ci" || action === "docker" || action === "devcontainer") {
380
490
  const pm = detectPackageManager(process.cwd());
381
491
  let langArg = detected;
382
492
  if (detected === "JavaScript/TypeScript") langArg = "JavaScript";
383
493
  if (detected === "Java/Kotlin") langArg = "Java";
384
494
 
385
- const steps =
386
- action === "ci"
387
- ? generateCI(process.cwd(), langArg, pm)
388
- : generateDocker(process.cwd(), langArg);
495
+ let steps = [];
496
+ if (action === "ci") steps = generateCI(process.cwd(), langArg, pm);
497
+ else if (action === "docker")
498
+ steps = generateDocker(process.cwd(), langArg);
499
+ else steps = generateDevContainer(process.cwd(), langArg);
500
+
501
+ const { kept, skipped } = filterExistingWriteFiles(
502
+ steps,
503
+ process.cwd()
504
+ );
389
505
 
390
- if (steps.length > 0) {
506
+ if (kept.length > 0) {
391
507
  console.log("\nApplying changes...");
392
- await runSteps(steps, { projectRoot: process.cwd() });
508
+ await runSteps(kept, { projectRoot: process.cwd() });
509
+ if (skipped.length > 0) {
510
+ console.log(
511
+ chalk.dim(`Skipped existing files: ${skipped.join(", ")}`)
512
+ );
513
+ }
393
514
  console.log(chalk.green("Done!"));
394
515
  } else {
395
516
  console.log(
@@ -401,6 +522,40 @@ async function main(options = {}) {
401
522
  return;
402
523
  }
403
524
 
525
+ if (action === "license") {
526
+ const { type, author } = await prompt([
527
+ {
528
+ type: "list",
529
+ name: "type",
530
+ message: "Choose a license:",
531
+ choices: licenseTypes,
532
+ },
533
+ {
534
+ type: "input",
535
+ name: "author",
536
+ message: "Author Name:",
537
+ default: effectiveConfig.author || "The Authors",
538
+ },
539
+ ]);
540
+ const steps = generateLicense(process.cwd(), type, author);
541
+ const { kept, skipped } = filterExistingWriteFiles(
542
+ steps,
543
+ process.cwd()
544
+ );
545
+ if (kept.length === 0) {
546
+ console.log(chalk.dim("Nothing to do (LICENSE already exists)."));
547
+ return;
548
+ }
549
+ await runSteps(kept, { projectRoot: process.cwd() });
550
+ if (skipped.length > 0) {
551
+ console.log(
552
+ chalk.dim(`Skipped existing files: ${skipped.join(", ")}`)
553
+ );
554
+ }
555
+ console.log(chalk.green("Done!"));
556
+ return;
557
+ }
558
+
404
559
  // If 'new', fall through to normal wizard
405
560
  }
406
561
  }
@@ -422,29 +577,80 @@ async function main(options = {}) {
422
577
  chalk.dim(" (Type to search)")
423
578
  );
424
579
 
425
- const languages = getLanguages();
580
+ const languages = getLanguages(effectiveConfig);
426
581
  if (languages.length === 0) {
427
582
  throw new Error("No languages configured.");
428
583
  }
429
584
 
430
- const userConfig = loadConfig();
431
-
432
585
  const allowedPms = ["npm", "pnpm", "yarn", "bun"];
433
586
  let preselectedPm =
434
587
  typeof args.pm === "string" && allowedPms.includes(args.pm)
435
588
  ? args.pm
436
589
  : undefined;
437
590
 
438
- if (!preselectedPm && userConfig.packageManager) {
439
- if (allowedPms.includes(userConfig.packageManager)) {
440
- 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;
441
599
  }
442
600
  }
443
601
 
444
- 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) {
445
624
  args.learning = true;
446
625
  }
447
626
 
627
+ const defaultLicenseType =
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
634
+ : null;
635
+
636
+ const defaultAuthor =
637
+ typeof effectiveConfig.author === "string" && effectiveConfig.author.trim()
638
+ ? effectiveConfig.author.trim()
639
+ : "The Authors";
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
+
448
654
  const state = {
449
655
  template: args.template || undefined,
450
656
  language:
@@ -457,7 +663,10 @@ async function main(options = {}) {
457
663
  };
458
664
 
459
665
  if (state.language) {
460
- const frameworksForLanguage = getFrameworks(state.language);
666
+ const frameworksForLanguage = getFrameworks(
667
+ state.language,
668
+ effectiveConfig
669
+ );
461
670
  state.framework =
462
671
  args.framework && frameworksForLanguage.includes(args.framework)
463
672
  ? args.framework
@@ -480,7 +689,7 @@ async function main(options = {}) {
480
689
  while (true) {
481
690
  if (step === "language") {
482
691
  const languageChoices = languages.map((lang) => {
483
- const count = getFrameworks(lang).length;
692
+ const count = getFrameworks(lang, effectiveConfig).length;
484
693
  return { name: `${lang} (${count})`, value: lang, short: lang };
485
694
  });
486
695
 
@@ -519,15 +728,19 @@ async function main(options = {}) {
519
728
  }
520
729
 
521
730
  if (step === "framework") {
522
- const frameworks = getFrameworks(state.language);
731
+ const frameworks = getFrameworks(state.language, effectiveConfig);
523
732
  if (frameworks.length === 0) {
524
733
  throw new Error(`No frameworks configured for ${state.language}.`);
525
734
  }
526
735
 
527
736
  const frameworkChoices = frameworks.map((fw) => {
528
- const gen = getGenerator(state.language, fw);
737
+ const gen = getGenerator(state.language, fw, effectiveConfig);
529
738
  const note = gen?.notes ? ` — ${gen.notes}` : "";
530
- 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 };
531
744
  });
532
745
 
533
746
  const frameworkQuestion = hasAutocomplete
@@ -566,7 +779,11 @@ async function main(options = {}) {
566
779
  state.framework = answer.framework;
567
780
 
568
781
  // Preflight Checks
569
- const gen = getGenerator(state.language, state.framework);
782
+ const gen = getGenerator(
783
+ state.language,
784
+ state.framework,
785
+ effectiveConfig
786
+ );
570
787
  if (gen && gen.check && gen.check.length > 0) {
571
788
  /* eslint-disable-next-line no-console */
572
789
  console.log(chalk.dim("\n(checking requirements...)"));
@@ -575,7 +792,7 @@ async function main(options = {}) {
575
792
 
576
793
  if (missing.length > 0) {
577
794
  console.log(chalk.red.bold("\nMissing required tools:"));
578
- missing.forEach((m) => console.log(chalk.red(` - ${m.bin}`)));
795
+ missing.forEach((m) => printMissingTool(m.bin));
579
796
  console.log(
580
797
  chalk.yellow("You may not be able to build or run this project.\n")
581
798
  );
@@ -737,6 +954,105 @@ async function main(options = {}) {
737
954
  // Remove .git to make it a fresh project
738
955
  removeGitFolder(projectRoot);
739
956
 
957
+ // Optional extras after cloning
958
+ const detectedTemplate = detectLanguage(projectRoot);
959
+ const pmTemplate = detectPackageManager(projectRoot);
960
+
961
+ let langArg = detectedTemplate;
962
+ if (detectedTemplate === "JavaScript/TypeScript")
963
+ langArg = "JavaScript";
964
+ if (detectedTemplate === "Java/Kotlin") langArg = "Java";
965
+
966
+ let wantCi = Boolean(args.ci) || defaultCi;
967
+ let wantDocker = Boolean(args.docker) || defaultDocker;
968
+ let wantDevContainer =
969
+ Boolean(args.devcontainer) || defaultDevContainer;
970
+ let wantLicense =
971
+ typeof args.license === "boolean"
972
+ ? args.license
973
+ : defaultLicenseType !== null;
974
+
975
+ if (!args.yes) {
976
+ const licenseLabel =
977
+ defaultLicenseType !== null
978
+ ? `LICENSE (${defaultLicenseType})`
979
+ : "LICENSE (skip)";
980
+
981
+ const { extras } = await prompt([
982
+ {
983
+ type: "checkbox",
984
+ name: "extras",
985
+ message: "Extras to apply after clone:",
986
+ choices: [
987
+ { name: "GitHub Actions CI", value: "ci", checked: wantCi },
988
+ {
989
+ name: "Dockerfile",
990
+ value: "docker",
991
+ checked: wantDocker,
992
+ },
993
+ {
994
+ name: "Dev Container (VS Code)",
995
+ value: "devcontainer",
996
+ checked: wantDevContainer,
997
+ },
998
+ {
999
+ name: licenseLabel,
1000
+ value: "license",
1001
+ checked: wantLicense,
1002
+ disabled:
1003
+ defaultLicenseType === null
1004
+ ? "Configure a default license in 'projectcli config'"
1005
+ : false,
1006
+ },
1007
+ ],
1008
+ },
1009
+ ]);
1010
+ if (extras) {
1011
+ wantCi = extras.includes("ci");
1012
+ wantDocker = extras.includes("docker");
1013
+ wantDevContainer = extras.includes("devcontainer");
1014
+ wantLicense = extras.includes("license");
1015
+ }
1016
+ }
1017
+
1018
+ const extraSteps = [];
1019
+ if (wantCi)
1020
+ extraSteps.push(...generateCI(projectRoot, langArg, pmTemplate));
1021
+ if (wantDocker)
1022
+ extraSteps.push(...generateDocker(projectRoot, langArg));
1023
+ if (wantDevContainer) {
1024
+ extraSteps.push(...generateDevContainer(projectRoot, langArg));
1025
+ }
1026
+ if (wantLicense && defaultLicenseType !== null) {
1027
+ extraSteps.push(
1028
+ ...generateLicense(projectRoot, defaultLicenseType, defaultAuthor)
1029
+ );
1030
+ }
1031
+
1032
+ if (wantLicense && defaultLicenseType === null) {
1033
+ console.log(
1034
+ chalk.dim(
1035
+ "Skipping LICENSE (no default license configured; run 'projectcli config')."
1036
+ )
1037
+ );
1038
+ }
1039
+
1040
+ if (extraSteps.length > 0) {
1041
+ const { kept, skipped } = filterExistingWriteFiles(
1042
+ extraSteps,
1043
+ projectRoot
1044
+ );
1045
+ if (kept.length > 0) {
1046
+ console.log(chalk.dim("\nApplying extras..."));
1047
+ await runSteps(kept, { projectRoot });
1048
+ }
1049
+ if (skipped.length > 0) {
1050
+ console.log(
1051
+ chalk.dim(`Skipped existing files: ${skipped.join(", ")}`)
1052
+ );
1053
+ }
1054
+ }
1055
+
740
1056
  console.log(
741
1057
  chalk.green(`\nSuccess! Created project at ${projectRoot}`)
742
1058
  );
@@ -746,7 +1062,14 @@ async function main(options = {}) {
746
1062
  )
747
1063
  );
748
1064
  } catch (err) {
749
- 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
+ }
750
1073
  process.exit(1);
751
1074
  }
752
1075
  return;
@@ -838,6 +1161,11 @@ async function main(options = {}) {
838
1161
  const message = err && err.message ? err.message : String(err);
839
1162
  console.error(`\nError: ${message}`);
840
1163
 
1164
+ const cmdNotFound = /^Command not found:\s*(.+)$/.exec(message);
1165
+ if (cmdNotFound && cmdNotFound[1]) {
1166
+ printMissingTool(cmdNotFound[1].trim());
1167
+ }
1168
+
841
1169
  const looksLikeNameIssue =
842
1170
  /cannot be used as a package name|Rust keyword|keyword|Cargo\.toml/i.test(
843
1171
  message
@@ -921,10 +1249,16 @@ async function main(options = {}) {
921
1249
  }
922
1250
  }
923
1251
 
924
- // CI/CD & Docker
1252
+ // CI/CD & Docker & DevContainer
925
1253
  if (!args.dryRun) {
926
- let wantCi = args.ci;
927
- let wantDocker = args.docker;
1254
+ let wantCi = Boolean(args.ci) || defaultCi;
1255
+ let wantDocker = Boolean(args.docker) || defaultDocker;
1256
+ let wantDevContainer =
1257
+ Boolean(args.devcontainer) || defaultDevContainer;
1258
+ let wantLicense =
1259
+ typeof args.license === "boolean"
1260
+ ? args.license
1261
+ : defaultLicenseType !== null;
928
1262
 
929
1263
  if (!args.yes) {
930
1264
  const { extras } = await prompt([
@@ -935,12 +1269,31 @@ async function main(options = {}) {
935
1269
  choices: [
936
1270
  { name: "GitHub Actions CI", value: "ci", checked: wantCi },
937
1271
  { name: "Dockerfile", value: "docker", checked: wantDocker },
1272
+ {
1273
+ name: "Dev Container (VS Code)",
1274
+ value: "devcontainer",
1275
+ checked: wantDevContainer,
1276
+ },
1277
+ {
1278
+ name:
1279
+ defaultLicenseType !== null
1280
+ ? `LICENSE (${defaultLicenseType})`
1281
+ : "LICENSE (skip)",
1282
+ value: "license",
1283
+ checked: wantLicense,
1284
+ disabled:
1285
+ defaultLicenseType === null
1286
+ ? "Configure a default license in 'projectcli config'"
1287
+ : false,
1288
+ },
938
1289
  ],
939
1290
  },
940
1291
  ]);
941
1292
  if (extras) {
942
1293
  wantCi = extras.includes("ci");
943
1294
  wantDocker = extras.includes("docker");
1295
+ wantDevContainer = extras.includes("devcontainer");
1296
+ wantLicense = extras.includes("license");
944
1297
  }
945
1298
  }
946
1299
 
@@ -951,10 +1304,28 @@ async function main(options = {}) {
951
1304
  if (wantDocker) {
952
1305
  extraSteps.push(...generateDocker(projectRoot, state.language));
953
1306
  }
1307
+ if (wantDevContainer) {
1308
+ extraSteps.push(...generateDevContainer(projectRoot, state.language));
1309
+ }
1310
+ if (wantLicense && defaultLicenseType !== null) {
1311
+ extraSteps.push(
1312
+ ...generateLicense(projectRoot, defaultLicenseType, defaultAuthor)
1313
+ );
1314
+ }
954
1315
 
955
- if (extraSteps.length > 0) {
1316
+ const { kept, skipped } = filterExistingWriteFiles(
1317
+ extraSteps,
1318
+ projectRoot
1319
+ );
1320
+
1321
+ if (kept.length > 0) {
956
1322
  console.log("Adding extras...");
957
- await runSteps(extraSteps, { projectRoot });
1323
+ await runSteps(kept, { projectRoot });
1324
+ }
1325
+ if (skipped.length > 0) {
1326
+ console.log(
1327
+ chalk.dim(`Skipped existing files: ${skipped.join(", ")}`)
1328
+ );
958
1329
  }
959
1330
  }
960
1331