@dawitworku/projectcli 0.2.0 โ†’ 0.2.2

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/README.md CHANGED
@@ -1,75 +1,118 @@
1
- # projectcli
1
+ # ProjectCLI ๐Ÿš€
2
2
 
3
- Interactive project generator.
3
+ > **The Swiss Army Knife for Project Scaffolding.**
4
+ > Bootstrapping new projects shouldn't require memorizing 50 different CLI commands.
4
5
 
5
- Run it as a single command, pick language โ†’ framework, and it scaffolds the project by calling the underlying official CLIs (Vite, Next.js, Nest, etc.).
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
+ [![npm version](https://badge.fury.io/js/@dawitworku%2Fprojectcli.svg)](https://badge.fury.io/js/@dawitworku%2Fprojectcli)
8
+ [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com)
6
9
 
7
- ## Run with npx (recommended)
10
+ **ProjectCLI** is an interactive, cross-language project generator. Instead of remembering usage for `create-react-app`, `cargo new`, `poetry new`, `laravel new`, `rails new`, etc., just run `projectcli`.
8
11
 
9
- After you publish this package to npm:
12
+ We handle the complexity of calling the official CLIs for you.
13
+
14
+ ## โœจ Features
15
+
16
+ - **Multi-Language Support**: Rust, Go, Python, JavaScript, TypeScript, PHP, Java, C#, Ruby, Swift, Dart.
17
+ - **Unified Interface**: One interactive wizard to rule them all.
18
+ - **Smart Context Awareness**: Running `projectcli` inside an existing project automatically offers to add libraries, CI/CD, or Dockerfiles tailored to that language.
19
+ - **Preflight Checks**: Warns you if you are missing required tools (e.g. `cargo`, `go`, `node`) before you start.
20
+ - **Remote Templates**: Clone any GitHub repository and automatically strip `.git` history for a fresh start.
21
+ - **CI/CD & Docker**: One-click generation of GitHub Actions workflows and Dockerfiles.
22
+ - **Dev Containers**: Generate `.devcontainer/devcontainer.json` for VS Code / Codespaces.
23
+ - **License Generator**: Add a standard `LICENSE` file (configurable default).
24
+
25
+ ## ๐Ÿš€ Quick Start
26
+
27
+ Run instantly with `npx`:
10
28
 
11
29
  ```bash
12
30
  npx @dawitworku/projectcli@latest
13
31
  ```
14
32
 
15
- You can also run non-interactively:
33
+ Or install globally:
16
34
 
17
35
  ```bash
18
- npx @dawitworku/projectcli@latest --language "TypeScript" --framework "NestJS" --name my-api --pm npm
36
+ npm install -g @dawitworku/projectcli
37
+ projectcli
19
38
  ```
20
39
 
21
- ## Run (dev)
40
+ ## ๐ŸŽฎ Interactive Mode
41
+
42
+ Just run `projectcli` and follow the prompts:
43
+
44
+ 1. Select **Language** (fuzzy search supported).
45
+ 2. Select **Framework** (React, Vue, Next.js, Actix, Axum, Django, FastAPI, etc.).
46
+ 3. Choose **Project Name**.
47
+ 4. (Optional) Add **CI/CD** or **Docker**.
48
+
49
+ ## ๐Ÿ›  Advanced Usage
50
+
51
+ ### Context Awareness
52
+
53
+ Run it inside a project to detect the language and offer relevant tools:
22
54
 
23
55
  ```bash
24
- npm install
25
- npm start
56
+ cd my-rust-project
57
+ projectcli
58
+ # Output: "โœ“ Detected active Rust project"
59
+ # Options: [Add GitHub Actions CI], [Add Dockerfile], [Add Dependencies]
26
60
  ```
27
61
 
28
- ## Install as a single-word command
62
+ ### Remote Templates
29
63
 
30
- From this repo folder:
64
+ Clone a starter kit from GitHub and make it your own instantly:
31
65
 
32
66
  ```bash
33
- npm install
34
- npm link
67
+ projectcli --template https://github.com/example/starter-repo --name my-app
35
68
  ```
36
69
 
37
- Then you can run:
70
+ ### Automation / CI Use
71
+
72
+ Skip the interactive prompts for scripts or specialized workflows:
38
73
 
39
74
  ```bash
40
- projectcli
75
+ projectcli --language Rust --framework Actix --name my-api --ci --docker --yes
41
76
  ```
42
77
 
43
- ## Add libraries to an existing project
78
+ ### Configuration
44
79
 
45
- Run inside a project folder:
80
+ Save your preferences (like default package manager):
46
81
 
47
82
  ```bash
48
- projectcli add
83
+ projectcli config
49
84
  ```
50
85
 
51
- ## Notes
86
+ You can set defaults like:
52
87
 
53
- - If you type a project name that already exists, the CLI will ask for another name (it wonโ€™t quit).
54
- - Some generators (like Vite/Next/etc.) can still ask their own questions โ€” those prompts come from the underlying tool.
88
+ - JS/TS package manager
89
+ - Author name (for LICENSE)
90
+ - Default license type (MIT/Apache2/ISC)
55
91
 
56
- ## Useful flags
92
+ ## ๐Ÿ“ฆ Supported Generators (Partial List)
57
93
 
58
- ```bash
59
- projectcli --help
60
- projectcli --version
61
- projectcli --list
62
- projectcli --language "JavaScript" --framework "Astro" --name myapp --pm pnpm
63
- projectcli --language "TypeScript" --framework "TanStack Start" --name myapp --pm npm
64
- projectcli --language "TypeScript" --framework "NestJS" --name myapi --pm pnpm
65
- projectcli --dry-run --language "JavaScript" --framework "Vite (React)" --name demo --pm pnpm
66
- ```
94
+ | Language | Frameworks / Tools |
95
+ | ------------------------- | ------------------------------------------------------------- |
96
+ | **JavaScript/TypeScript** | React (Vite), Vue, Next.js, NestJS, Express, Astro, Svelte... |
97
+ | **Rust** | Binary, Library, Actix Web, Axum, Rocket, Taurus... |
98
+ | **Python** | Poetry, Setuptools, Django, Flask, FastAPI... |
99
+ | **Go** | Binary, Fiber, Gin, Chi, Echo... |
100
+ | **PHP** | Laravel, Symfony, Slim... |
101
+ | **Java/Kotlin** | Spring Boot, Gradle/Maven... |
102
+ | **...and more** | C#, Ruby, Swift, Dart |
103
+
104
+ ## ๐Ÿค Contributing
105
+
106
+ We love contributions! Whether it's adding a new framework to the registry or fixing a bug.
107
+
108
+ 1. Fork it.
109
+ 2. Create your feature branch (`git checkout -b feature/new-framework`).
110
+ 3. Commit your changes (`git commit -am 'Add support for X'`).
111
+ 4. Push to the branch (`git push origin feature/new-framework`).
112
+ 5. Create a new Pull Request.
67
113
 
68
- ## How it works
114
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for more details.
69
115
 
70
- - Choose language
71
- - Choose framework
72
- - Enter project name
73
- - CLI runs the underlying generator commands (npm/cargo/django-admin/etc.)
116
+ ## ๐Ÿ“ License
74
117
 
75
- If a generator command is missing on your machine, install it and re-run.
118
+ MIT ยฉ [Dawit Worku](https://github.com/dawitworku)
package/package.json CHANGED
@@ -1,7 +1,16 @@
1
1
  {
2
2
  "name": "@dawitworku/projectcli",
3
- "version": "0.2.0",
4
- "description": "Interactive project generator (language -> framework -> create).",
3
+ "version": "0.2.2",
4
+ "description": "The ultimate interactive project generator (language -> framework -> create).",
5
+ "author": "Dawit Worku",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/dawitworku/projectcli"
9
+ },
10
+ "bugs": {
11
+ "url": "https://github.com/dawitworku/projectcli/issues"
12
+ },
13
+ "homepage": "https://github.com/dawitworku/projectcli#readme",
5
14
  "license": "MIT",
6
15
  "type": "commonjs",
7
16
  "bin": {
@@ -23,7 +32,7 @@
23
32
  ],
24
33
  "scripts": {
25
34
  "start": "node bin/projectcli.js",
26
- "lint": "node -c bin/projectcli.js && node -c src/index.js && node -c src/registry.js && node -c src/run.js"
35
+ "lint": "node -c bin/projectcli.js && node -c src/index.js && node -c src/registry.js && node -c src/run.js && node -c src/settings.js && node -c src/config.js && node -c src/devcontainer.js && node -c src/license.js"
27
36
  },
28
37
  "dependencies": {
29
38
  "boxen": "^5.1.2",
package/src/config.js ADDED
@@ -0,0 +1,34 @@
1
+ const fs = require("node:fs");
2
+ const path = require("node:path");
3
+ const os = require("node:os");
4
+
5
+ const CONFIG_PATH = path.join(os.homedir(), ".projectcli.json");
6
+
7
+ function loadConfig() {
8
+ try {
9
+ if (fs.existsSync(CONFIG_PATH)) {
10
+ const content = fs.readFileSync(CONFIG_PATH, "utf8");
11
+ return JSON.parse(content);
12
+ }
13
+ } catch (err) {
14
+ // ignore
15
+ }
16
+ return {};
17
+ }
18
+
19
+ function saveConfig(data) {
20
+ try {
21
+ const current = loadConfig();
22
+ const updated = { ...current, ...data };
23
+ fs.writeFileSync(CONFIG_PATH, JSON.stringify(updated, null, 2));
24
+ return true;
25
+ } catch (err) {
26
+ return false;
27
+ }
28
+ }
29
+
30
+ module.exports = {
31
+ loadConfig,
32
+ saveConfig,
33
+ CONFIG_PATH,
34
+ };
@@ -0,0 +1,82 @@
1
+ const DEVCONTAINER_NODE = `{
2
+ "name": "Node.js",
3
+ "image": "mcr.microsoft.com/devcontainers/javascript-node:20",
4
+ "features": {
5
+ "ghcr.io/devcontainers/features/node:1": {}
6
+ },
7
+ "forwardPorts": [3000],
8
+ "postCreateCommand": "npm install"
9
+ }`;
10
+
11
+ const DEVCONTAINER_PYTHON = `{
12
+ "name": "Python 3",
13
+ "image": "mcr.microsoft.com/devcontainers/python:3.11",
14
+ "features": {
15
+ "ghcr.io/devcontainers/features/python:1": {}
16
+ },
17
+ "postCreateCommand": "pip install -r requirements.txt"
18
+ }`;
19
+
20
+ const DEVCONTAINER_RUST = `{
21
+ "name": "Rust",
22
+ "image": "mcr.microsoft.com/devcontainers/rust:1",
23
+ "features": {
24
+ "ghcr.io/devcontainers/features/rust:1": {}
25
+ },
26
+ "customizations": {
27
+ "vscode": {
28
+ "extensions": ["rust-lang.rust-analyzer", "tamasfe.even-better-toml"]
29
+ }
30
+ },
31
+ "postCreateCommand": "cargo build"
32
+ }`;
33
+
34
+ const DEVCONTAINER_GO = `{
35
+ "name": "Go",
36
+ "image": "mcr.microsoft.com/devcontainers/go:1.21",
37
+ "features": {
38
+ "ghcr.io/devcontainers/features/go:1": {}
39
+ },
40
+ "customizations": {
41
+ "vscode": {
42
+ "extensions": ["golang.Go"]
43
+ }
44
+ },
45
+ "postCreateCommand": "go mod download"
46
+ }`;
47
+
48
+ function getDevContainer(language) {
49
+ if (language === "JavaScript" || language === "TypeScript")
50
+ return DEVCONTAINER_NODE;
51
+ if (language === "Python") return DEVCONTAINER_PYTHON;
52
+ if (language === "Rust") return DEVCONTAINER_RUST;
53
+ if (language === "Go") return DEVCONTAINER_GO;
54
+
55
+ // Generic fallback
56
+ return `{
57
+ "name": "Default",
58
+ "image": "mcr.microsoft.com/devcontainers/base:ubuntu",
59
+ "features": {
60
+ "ghcr.io/devcontainers/features/common-utils:2": {}
61
+ }
62
+ }`;
63
+ }
64
+
65
+ function generateDevContainer(projectRoot, language) {
66
+ const content = getDevContainer(language);
67
+ return [
68
+ {
69
+ type: "mkdir",
70
+ path: ".devcontainer",
71
+ },
72
+ {
73
+ type: "writeFile",
74
+ path: ".devcontainer/devcontainer.json",
75
+ content: content,
76
+ },
77
+ ];
78
+ }
79
+
80
+ module.exports = {
81
+ generateDevContainer,
82
+ };
package/src/index.js CHANGED
@@ -28,8 +28,15 @@ 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");
32
+ const { gitClone, removeGitFolder } = require("./remote");
31
33
  const { generateCI, generateDocker } = require("./cicd");
34
+ const { generateDevContainer } = require("./devcontainer");
35
+ const { generateLicense, licenseTypes } = require("./license");
32
36
  const { getDescription } = require("./descriptions");
37
+ const { detectLanguage, detectPackageManager } = require("./detect");
38
+ const { loadConfig } = require("./config");
39
+ const { runConfig } = require("./settings");
33
40
 
34
41
  const RUST_KEYWORDS = new Set(
35
42
  [
@@ -133,7 +140,10 @@ function parseArgs(argv) {
133
140
  pm: undefined,
134
141
  ci: false,
135
142
  docker: false,
143
+ devcontainer: false,
144
+ license: undefined,
136
145
  learning: false,
146
+ template: undefined,
137
147
  };
138
148
 
139
149
  const nextValue = (i) => {
@@ -150,8 +160,16 @@ function parseArgs(argv) {
150
160
  else if (a === "--dry-run") out.dryRun = true;
151
161
  else if (a === "--ci") out.ci = true;
152
162
  else if (a === "--docker") out.docker = true;
163
+ else if (a === "--devcontainer") out.devcontainer = true;
164
+ else if (a === "--license") out.license = true;
165
+ else if (a === "--no-license") out.license = false;
153
166
  else if (a === "--learning") out.learning = true;
154
- else if (a.startsWith("--language="))
167
+ else if (a.startsWith("--template="))
168
+ out.template = a.slice("--template=".length);
169
+ else if (a === "--template") {
170
+ out.template = nextValue(i);
171
+ i++;
172
+ } else if (a.startsWith("--language="))
155
173
  out.language = a.slice("--language=".length);
156
174
  else if (a === "--language") {
157
175
  out.language = nextValue(i);
@@ -197,6 +215,7 @@ function printHelp() {
197
215
  console.log(
198
216
  " projectcli --language <lang> --framework <fw> --name <project>"
199
217
  );
218
+ console.log(" projectcli config # configure defaults");
200
219
  console.log("");
201
220
  console.log("Flags:");
202
221
  console.log(" --help, -h Show help");
@@ -212,10 +231,15 @@ function printHelp() {
212
231
  );
213
232
  console.log(" --ci Auto-add GitHub Actions CI");
214
233
  console.log(" --docker Auto-add Dockerfile");
234
+ console.log(" --devcontainer Auto-add VS Code Dev Container");
235
+ console.log(" --license Force-add LICENSE (uses config defaults)");
236
+ console.log(" --no-license Never add LICENSE");
215
237
  console.log(" --learning Enable learning mode (shows descriptions)");
238
+ console.log(" --template Clone from a Git repository URL");
216
239
  }
217
240
 
218
241
  const BACK = "__back__";
242
+ const COMMUNITY = "Community / Remote";
219
243
 
220
244
  function withBack(choices) {
221
245
  return [
@@ -247,6 +271,22 @@ function printStepsPreview(steps) {
247
271
  console.log("");
248
272
  }
249
273
 
274
+ function filterExistingWriteFiles(steps, projectRoot) {
275
+ const kept = [];
276
+ const skipped = [];
277
+ for (const step of steps || []) {
278
+ if (step && step.type === "writeFile" && typeof step.path === "string") {
279
+ const target = path.resolve(projectRoot, step.path);
280
+ if (fs.existsSync(target)) {
281
+ skipped.push(step.path);
282
+ continue;
283
+ }
284
+ }
285
+ kept.push(step);
286
+ }
287
+ return { kept, skipped };
288
+ }
289
+
250
290
  function printList() {
251
291
  for (const lang of getLanguages()) {
252
292
  console.log(`${lang}:`);
@@ -310,6 +350,134 @@ async function main(options = {}) {
310
350
  return;
311
351
  }
312
352
 
353
+ if (cmd === "config") {
354
+ await runConfig({ prompt });
355
+ return;
356
+ }
357
+
358
+ // Smart Context Detection
359
+ if (
360
+ cmd === "init" &&
361
+ rest.length === 0 &&
362
+ !args.language &&
363
+ !args.framework &&
364
+ !args.template &&
365
+ !args.name
366
+ ) {
367
+ const detected = detectLanguage(process.cwd());
368
+ if (detected && detected !== "Unknown") {
369
+ console.clear();
370
+ console.log(
371
+ gradient.pastel.multiline(
372
+ figlet.textSync("ProjectCLI", { font: "Standard" })
373
+ )
374
+ );
375
+ console.log(chalk.bold.magenta(" The Swiss Army Knife for Developers"));
376
+ console.log("");
377
+ console.log(
378
+ chalk.green(`โœ“ Detected active ${chalk.bold(detected)} project.`)
379
+ );
380
+
381
+ const { action } = await prompt([
382
+ {
383
+ type: "list",
384
+ name: "action",
385
+ message: "What would you like to do?",
386
+ choices: [
387
+ { name: "Add Library / Dependency", value: "add" },
388
+ { name: "Add GitHub Actions CI", value: "ci" },
389
+ { name: "Add Dockerfile", value: "docker" },
390
+ { name: "Add Dev Container (VS Code)", value: "devcontainer" },
391
+ { name: "Add License", value: "license" },
392
+ new inquirer.Separator(),
393
+ { name: "Start New Project Here", value: "new" },
394
+ { name: "Exit", value: "exit" },
395
+ ],
396
+ },
397
+ ]);
398
+
399
+ if (action === "exit") process.exit(0);
400
+
401
+ if (action === "add") {
402
+ // reuse the add logic, passing empty args so it detects locally
403
+ await runAdd({ prompt, argv: [] });
404
+ return;
405
+ }
406
+
407
+ if (action === "ci" || action === "docker" || action === "devcontainer") {
408
+ const pm = detectPackageManager(process.cwd());
409
+ let langArg = detected;
410
+ if (detected === "JavaScript/TypeScript") langArg = "JavaScript";
411
+ if (detected === "Java/Kotlin") langArg = "Java";
412
+
413
+ let steps = [];
414
+ if (action === "ci") steps = generateCI(process.cwd(), langArg, pm);
415
+ else if (action === "docker")
416
+ steps = generateDocker(process.cwd(), langArg);
417
+ else steps = generateDevContainer(process.cwd(), langArg);
418
+
419
+ const { kept, skipped } = filterExistingWriteFiles(
420
+ steps,
421
+ process.cwd()
422
+ );
423
+
424
+ if (kept.length > 0) {
425
+ console.log("\nApplying changes...");
426
+ await runSteps(kept, { projectRoot: process.cwd() });
427
+ if (skipped.length > 0) {
428
+ console.log(
429
+ chalk.dim(`Skipped existing files: ${skipped.join(", ")}`)
430
+ );
431
+ }
432
+ console.log(chalk.green("Done!"));
433
+ } else {
434
+ console.log(
435
+ chalk.yellow(
436
+ "No standard template available for this language yet."
437
+ )
438
+ );
439
+ }
440
+ return;
441
+ }
442
+
443
+ if (action === "license") {
444
+ const { type, author } = await prompt([
445
+ {
446
+ type: "list",
447
+ name: "type",
448
+ message: "Choose a license:",
449
+ choices: licenseTypes,
450
+ },
451
+ {
452
+ type: "input",
453
+ name: "author",
454
+ message: "Author Name:",
455
+ default: userConfig.author || "The Authors",
456
+ },
457
+ ]);
458
+ const steps = generateLicense(process.cwd(), type, author);
459
+ const { kept, skipped } = filterExistingWriteFiles(
460
+ steps,
461
+ process.cwd()
462
+ );
463
+ if (kept.length === 0) {
464
+ console.log(chalk.dim("Nothing to do (LICENSE already exists)."));
465
+ return;
466
+ }
467
+ await runSteps(kept, { projectRoot: process.cwd() });
468
+ if (skipped.length > 0) {
469
+ console.log(
470
+ chalk.dim(`Skipped existing files: ${skipped.join(", ")}`)
471
+ );
472
+ }
473
+ console.log(chalk.green("Done!"));
474
+ return;
475
+ }
476
+
477
+ // If 'new', fall through to normal wizard
478
+ }
479
+ }
480
+
313
481
  // Clear console for a fresh start
314
482
  console.clear();
315
483
 
@@ -332,13 +500,37 @@ async function main(options = {}) {
332
500
  throw new Error("No languages configured.");
333
501
  }
334
502
 
503
+ const userConfig = loadConfig();
504
+
335
505
  const allowedPms = ["npm", "pnpm", "yarn", "bun"];
336
- const preselectedPm =
506
+ let preselectedPm =
337
507
  typeof args.pm === "string" && allowedPms.includes(args.pm)
338
508
  ? args.pm
339
509
  : undefined;
340
510
 
511
+ if (!preselectedPm && userConfig.packageManager) {
512
+ if (allowedPms.includes(userConfig.packageManager)) {
513
+ preselectedPm = userConfig.packageManager;
514
+ }
515
+ }
516
+
517
+ if (userConfig.learningMode && args.learning === false) {
518
+ args.learning = true;
519
+ }
520
+
521
+ const defaultLicenseType =
522
+ typeof userConfig.defaultLicense === "string" &&
523
+ licenseTypes.includes(userConfig.defaultLicense)
524
+ ? userConfig.defaultLicense
525
+ : null;
526
+
527
+ const defaultAuthor =
528
+ typeof userConfig.author === "string" && userConfig.author.trim()
529
+ ? userConfig.author.trim()
530
+ : "The Authors";
531
+
341
532
  const state = {
533
+ template: args.template || undefined,
342
534
  language:
343
535
  args.language && languages.includes(args.language)
344
536
  ? args.language
@@ -360,7 +552,10 @@ async function main(options = {}) {
360
552
  state.language === "JavaScript" || state.language === "TypeScript";
361
553
 
362
554
  let step = "language";
363
- if (!state.language) step = "language";
555
+ if (state.template) {
556
+ if (!state.name) step = "name";
557
+ else step = "confirm";
558
+ } else if (!state.language) step = "language";
364
559
  else if (!state.framework) step = "framework";
365
560
  else if (needsPackageManager && !state.pm) step = "pm";
366
561
  else if (!state.name) step = "name";
@@ -453,6 +648,38 @@ async function main(options = {}) {
453
648
  }
454
649
 
455
650
  state.framework = answer.framework;
651
+
652
+ // Preflight Checks
653
+ const gen = getGenerator(state.language, state.framework);
654
+ if (gen && gen.check && gen.check.length > 0) {
655
+ /* eslint-disable-next-line no-console */
656
+ console.log(chalk.dim("\n(checking requirements...)"));
657
+ const results = await checkBinaries(gen.check);
658
+ const missing = results.filter((r) => !r.ok);
659
+
660
+ if (missing.length > 0) {
661
+ console.log(chalk.red.bold("\nMissing required tools:"));
662
+ missing.forEach((m) => console.log(chalk.red(` - ${m.bin}`)));
663
+ console.log(
664
+ chalk.yellow("You may not be able to build or run this project.\n")
665
+ );
666
+
667
+ const { proceed } = await prompt([
668
+ {
669
+ type: "confirm",
670
+ name: "proceed",
671
+ message: "Continue anyway?",
672
+ default: false,
673
+ },
674
+ ]);
675
+
676
+ if (!proceed) {
677
+ step = "framework";
678
+ continue;
679
+ }
680
+ }
681
+ }
682
+
456
683
  step = "pm";
457
684
  continue;
458
685
  }
@@ -528,6 +755,10 @@ async function main(options = {}) {
528
755
 
529
756
  const v = String(projectName || "").trim();
530
757
  if (v.toLowerCase() === "back") {
758
+ if (state.template) {
759
+ console.log("Operation cancelled.");
760
+ process.exit(0);
761
+ }
531
762
  step =
532
763
  state.language === "JavaScript" || state.language === "TypeScript"
533
764
  ? "pm"
@@ -541,13 +772,173 @@ async function main(options = {}) {
541
772
  }
542
773
 
543
774
  if (step === "confirm") {
775
+ const projectRoot = path.resolve(process.cwd(), state.name);
776
+ const targetExists = fs.existsSync(projectRoot);
777
+
778
+ if (state.template) {
779
+ if (targetExists && !args.dryRun) {
780
+ console.error(
781
+ `\nError: Target folder already exists: ${projectRoot}`
782
+ );
783
+ state.name = undefined;
784
+ step = "name";
785
+ continue;
786
+ }
787
+
788
+ console.log("\nProject Configuration:");
789
+ console.log(` Template: ${state.template}`);
790
+ console.log(` Folder: ${state.name}`);
791
+ console.log("");
792
+
793
+ if (!args.yes) {
794
+ const { action } = await prompt([
795
+ {
796
+ type: "list",
797
+ name: "action",
798
+ message: "Clone this template?",
799
+ choices: [
800
+ { name: "Clone template", value: "create" },
801
+ { name: "Cancel", value: "cancel" },
802
+ ],
803
+ },
804
+ ]);
805
+ if (action === "cancel") {
806
+ console.log("Aborted.");
807
+ return;
808
+ }
809
+ }
810
+
811
+ if (args.dryRun) {
812
+ console.log(
813
+ `[Dry Run] Would clone ${state.template} to ${projectRoot}`
814
+ );
815
+ return;
816
+ }
817
+
818
+ console.log(chalk.dim("\nCloning repository..."));
819
+ try {
820
+ await gitClone(state.template, projectRoot);
821
+ // Remove .git to make it a fresh project
822
+ removeGitFolder(projectRoot);
823
+
824
+ // Optional extras after cloning
825
+ const detectedTemplate = detectLanguage(projectRoot);
826
+ const pmTemplate = detectPackageManager(projectRoot);
827
+
828
+ let langArg = detectedTemplate;
829
+ if (detectedTemplate === "JavaScript/TypeScript")
830
+ langArg = "JavaScript";
831
+ if (detectedTemplate === "Java/Kotlin") langArg = "Java";
832
+
833
+ let wantCi = Boolean(args.ci);
834
+ let wantDocker = Boolean(args.docker);
835
+ let wantDevContainer = Boolean(args.devcontainer);
836
+ let wantLicense =
837
+ typeof args.license === "boolean"
838
+ ? args.license
839
+ : defaultLicenseType !== null;
840
+
841
+ if (!args.yes) {
842
+ const licenseLabel =
843
+ defaultLicenseType !== null
844
+ ? `LICENSE (${defaultLicenseType})`
845
+ : "LICENSE (skip)";
846
+
847
+ const { extras } = await prompt([
848
+ {
849
+ type: "checkbox",
850
+ name: "extras",
851
+ message: "Extras to apply after clone:",
852
+ choices: [
853
+ { name: "GitHub Actions CI", value: "ci", checked: wantCi },
854
+ {
855
+ name: "Dockerfile",
856
+ value: "docker",
857
+ checked: wantDocker,
858
+ },
859
+ {
860
+ name: "Dev Container (VS Code)",
861
+ value: "devcontainer",
862
+ checked: wantDevContainer,
863
+ },
864
+ {
865
+ name: licenseLabel,
866
+ value: "license",
867
+ checked: wantLicense,
868
+ disabled:
869
+ defaultLicenseType === null
870
+ ? "Configure a default license in 'projectcli config'"
871
+ : false,
872
+ },
873
+ ],
874
+ },
875
+ ]);
876
+ if (extras) {
877
+ wantCi = extras.includes("ci");
878
+ wantDocker = extras.includes("docker");
879
+ wantDevContainer = extras.includes("devcontainer");
880
+ wantLicense = extras.includes("license");
881
+ }
882
+ }
883
+
884
+ const extraSteps = [];
885
+ if (wantCi)
886
+ extraSteps.push(...generateCI(projectRoot, langArg, pmTemplate));
887
+ if (wantDocker)
888
+ extraSteps.push(...generateDocker(projectRoot, langArg));
889
+ if (wantDevContainer) {
890
+ extraSteps.push(...generateDevContainer(projectRoot, langArg));
891
+ }
892
+ if (wantLicense && defaultLicenseType !== null) {
893
+ extraSteps.push(
894
+ ...generateLicense(projectRoot, defaultLicenseType, defaultAuthor)
895
+ );
896
+ }
897
+
898
+ if (wantLicense && defaultLicenseType === null) {
899
+ console.log(
900
+ chalk.dim(
901
+ "Skipping LICENSE (no default license configured; run 'projectcli config')."
902
+ )
903
+ );
904
+ }
905
+
906
+ if (extraSteps.length > 0) {
907
+ const { kept, skipped } = filterExistingWriteFiles(
908
+ extraSteps,
909
+ projectRoot
910
+ );
911
+ if (kept.length > 0) {
912
+ console.log(chalk.dim("\nApplying extras..."));
913
+ await runSteps(kept, { projectRoot });
914
+ }
915
+ if (skipped.length > 0) {
916
+ console.log(
917
+ chalk.dim(`Skipped existing files: ${skipped.join(", ")}`)
918
+ );
919
+ }
920
+ }
921
+
922
+ console.log(
923
+ chalk.green(`\nSuccess! Created project at ${projectRoot}`)
924
+ );
925
+ console.log(
926
+ chalk.dim(
927
+ "You may need to run 'npm install' or similar inside the folder."
928
+ )
929
+ );
930
+ } catch (err) {
931
+ console.error(chalk.red("\nFailed to clone template:"), err.message);
932
+ process.exit(1);
933
+ }
934
+ return;
935
+ }
936
+
544
937
  const generator = getGenerator(state.language, state.framework);
545
938
  if (!generator) {
546
939
  throw new Error("Generator not found (registry mismatch).");
547
940
  }
548
941
 
549
- const projectRoot = path.resolve(process.cwd(), state.name);
550
- const targetExists = fs.existsSync(projectRoot);
551
942
  if (targetExists && !args.dryRun) {
552
943
  console.error(`\nError: Target folder already exists: ${projectRoot}`);
553
944
  state.name = undefined;
@@ -712,10 +1103,15 @@ async function main(options = {}) {
712
1103
  }
713
1104
  }
714
1105
 
715
- // CI/CD & Docker
1106
+ // CI/CD & Docker & DevContainer
716
1107
  if (!args.dryRun) {
717
1108
  let wantCi = args.ci;
718
1109
  let wantDocker = args.docker;
1110
+ let wantDevContainer = Boolean(args.devcontainer);
1111
+ let wantLicense =
1112
+ typeof args.license === "boolean"
1113
+ ? args.license
1114
+ : defaultLicenseType !== null;
719
1115
 
720
1116
  if (!args.yes) {
721
1117
  const { extras } = await prompt([
@@ -726,12 +1122,31 @@ async function main(options = {}) {
726
1122
  choices: [
727
1123
  { name: "GitHub Actions CI", value: "ci", checked: wantCi },
728
1124
  { name: "Dockerfile", value: "docker", checked: wantDocker },
1125
+ {
1126
+ name: "Dev Container (VS Code)",
1127
+ value: "devcontainer",
1128
+ checked: wantDevContainer,
1129
+ },
1130
+ {
1131
+ name:
1132
+ defaultLicenseType !== null
1133
+ ? `LICENSE (${defaultLicenseType})`
1134
+ : "LICENSE (skip)",
1135
+ value: "license",
1136
+ checked: wantLicense,
1137
+ disabled:
1138
+ defaultLicenseType === null
1139
+ ? "Configure a default license in 'projectcli config'"
1140
+ : false,
1141
+ },
729
1142
  ],
730
1143
  },
731
1144
  ]);
732
1145
  if (extras) {
733
1146
  wantCi = extras.includes("ci");
734
1147
  wantDocker = extras.includes("docker");
1148
+ wantDevContainer = extras.includes("devcontainer");
1149
+ wantLicense = extras.includes("license");
735
1150
  }
736
1151
  }
737
1152
 
@@ -742,10 +1157,28 @@ async function main(options = {}) {
742
1157
  if (wantDocker) {
743
1158
  extraSteps.push(...generateDocker(projectRoot, state.language));
744
1159
  }
1160
+ if (wantDevContainer) {
1161
+ extraSteps.push(...generateDevContainer(projectRoot, state.language));
1162
+ }
1163
+ if (wantLicense && defaultLicenseType !== null) {
1164
+ extraSteps.push(
1165
+ ...generateLicense(projectRoot, defaultLicenseType, defaultAuthor)
1166
+ );
1167
+ }
745
1168
 
746
- if (extraSteps.length > 0) {
1169
+ const { kept, skipped } = filterExistingWriteFiles(
1170
+ extraSteps,
1171
+ projectRoot
1172
+ );
1173
+
1174
+ if (kept.length > 0) {
747
1175
  console.log("Adding extras...");
748
- await runSteps(extraSteps, { projectRoot });
1176
+ await runSteps(kept, { projectRoot });
1177
+ }
1178
+ if (skipped.length > 0) {
1179
+ console.log(
1180
+ chalk.dim(`Skipped existing files: ${skipped.join(", ")}`)
1181
+ );
749
1182
  }
750
1183
  }
751
1184
 
package/src/license.js ADDED
@@ -0,0 +1,80 @@
1
+ const licenses = {
2
+ MIT: (year, author) => `MIT License
3
+
4
+ Copyright (c) ${year} ${author}
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
23
+ `,
24
+ Apache2: (year, author) => ` Apache License
25
+ Version 2.0, January 2004
26
+ http://www.apache.org/licenses/
27
+
28
+ Copyright ${year} ${author}
29
+
30
+ Licensed under the Apache License, Version 2.0 (the "License");
31
+ you may not use this file except in compliance with the License.
32
+ You may obtain a copy of the License at
33
+
34
+ http://www.apache.org/licenses/LICENSE-2.0
35
+
36
+ Unless required by applicable law or agreed to in writing, software
37
+ distributed under the License is distributed on an "AS IS" BASIS,
38
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
39
+ See the License for the specific language governing permissions and
40
+ limitations under the License.
41
+ `,
42
+ ISC: (year, author) => `ISC License
43
+
44
+ Copyright (c) ${year}, ${author}
45
+
46
+ Permission to use, copy, modify, and/or distribute this software for any
47
+ purpose with or without fee is hereby granted, provided that the above
48
+ copyright notice and this permission notice appear in all copies.
49
+
50
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
51
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
52
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
53
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
54
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
55
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
56
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
57
+ `,
58
+ };
59
+
60
+ function getLicense(type, author) {
61
+ const year = new Date().getFullYear();
62
+ const template = licenses[type] || licenses.MIT;
63
+ return template(year, author || "The Authors");
64
+ }
65
+
66
+ function generateLicense(projectRoot, type, author) {
67
+ return [
68
+ {
69
+ type: "writeFile",
70
+ path: "LICENSE",
71
+ content: getLicense(type, author),
72
+ },
73
+ ];
74
+ }
75
+
76
+ module.exports = {
77
+ getLicense,
78
+ generateLicense,
79
+ licenseTypes: ["MIT", "Apache2", "ISC"],
80
+ };
@@ -0,0 +1,25 @@
1
+ const { spawn } = require("node:child_process");
2
+
3
+ function checkBinary(binary, args = ["--version"]) {
4
+ return new Promise((resolve) => {
5
+ const child = spawn(binary, args, {
6
+ stdio: "ignore",
7
+ shell: false,
8
+ });
9
+ child.on("error", () => resolve(false));
10
+ child.on("close", (code) => resolve(code === 0));
11
+ });
12
+ }
13
+
14
+ function checkBinaries(binaries) {
15
+ if (!binaries || binaries.length === 0) return [];
16
+ const checks = binaries.map((bin) =>
17
+ checkBinary(bin).then((ok) => ({ bin, ok }))
18
+ );
19
+ return Promise.all(checks);
20
+ }
21
+
22
+ module.exports = {
23
+ checkBinary,
24
+ checkBinaries,
25
+ };
package/src/registry.js CHANGED
@@ -33,6 +33,7 @@ const REGISTRY = {
33
33
  "Vite (Vanilla)": {
34
34
  id: "js.vite.vanilla",
35
35
  label: "Vite (Vanilla)",
36
+ check: ["npm"],
36
37
  commands: (ctx) => {
37
38
  const projectName = getProjectName(ctx);
38
39
  const pm = getPackageManager(ctx);
@@ -409,6 +410,7 @@ const REGISTRY = {
409
410
  Django: {
410
411
  id: "py.django",
411
412
  label: "Django",
413
+ check: ["django-admin"],
412
414
  commands: (ctx) => {
413
415
  const projectName = getProjectName(ctx);
414
416
  return [
@@ -423,6 +425,7 @@ const REGISTRY = {
423
425
  Flask: {
424
426
  id: "py.flask.basic",
425
427
  label: "Flask (basic)",
428
+ check: ["python3"],
426
429
  commands: (_ctx) => [
427
430
  { type: "mkdir", path: "." },
428
431
  {
@@ -502,6 +505,7 @@ const REGISTRY = {
502
505
  "Cargo (bin)": {
503
506
  id: "rs.cargo.bin",
504
507
  label: "Cargo (bin)",
508
+ check: ["cargo"],
505
509
  commands: (ctx) => {
506
510
  const projectName = getProjectName(ctx);
507
511
  return [
@@ -516,6 +520,7 @@ const REGISTRY = {
516
520
  "Cargo (lib)": {
517
521
  id: "rs.cargo.lib",
518
522
  label: "Cargo (lib)",
523
+ check: ["cargo"],
519
524
  commands: (ctx) => {
520
525
  const projectName = getProjectName(ctx);
521
526
  return [
@@ -558,6 +563,7 @@ const REGISTRY = {
558
563
  "Go module (basic)": {
559
564
  id: "go.module.basic",
560
565
  label: "Go module (basic)",
566
+ check: ["go"],
561
567
  commands: (ctx) => {
562
568
  const projectName = getProjectName(ctx);
563
569
  return [
package/src/remote.js ADDED
@@ -0,0 +1,39 @@
1
+ const { spawn } = require("node:child_process");
2
+
3
+ function gitClone(url, targetDir) {
4
+ return new Promise((resolve, reject) => {
5
+ // git clone --depth 1 url targetDir
6
+ const child = spawn("git", ["clone", "--depth", "1", url, targetDir], {
7
+ stdio: "inherit",
8
+ shell: false,
9
+ });
10
+
11
+ child.on("error", (err) => reject(err));
12
+ child.on("close", (code) => {
13
+ if (code === 0) resolve();
14
+ else reject(new Error(`Git clone failed with code ${code}`));
15
+ });
16
+ });
17
+ }
18
+
19
+ /**
20
+ * Removes the .git folder from the target directory so it becomes a fresh project.
21
+ */
22
+ function removeGitFolder(targetDir) {
23
+ const fs = require("node:fs");
24
+ const path = require("node:path");
25
+ const gitPath = path.join(targetDir, ".git");
26
+
27
+ try {
28
+ if (fs.existsSync(gitPath)) {
29
+ fs.rmSync(gitPath, { recursive: true, force: true });
30
+ }
31
+ } catch (err) {
32
+ // ignore
33
+ }
34
+ }
35
+
36
+ module.exports = {
37
+ gitClone,
38
+ removeGitFolder,
39
+ };
@@ -0,0 +1,63 @@
1
+ const chalk = require("chalk");
2
+ const { loadConfig, saveConfig, CONFIG_PATH } = require("./config");
3
+
4
+ async function runConfig({ prompt }) {
5
+ const config = loadConfig();
6
+
7
+ console.log(chalk.bold.cyan("\nConfiguration Settings"));
8
+ console.log(chalk.dim(`File: ${CONFIG_PATH}\n`));
9
+
10
+ const answers = await prompt([
11
+ {
12
+ type: "list",
13
+ name: "packageManager",
14
+ message: "Default Package Manager (for JS/TS):",
15
+ choices: [
16
+ { name: "None (Always ask)", value: null },
17
+ inquirerSeparator(),
18
+ { name: "npm", value: "npm" },
19
+ { name: "pnpm", value: "pnpm" },
20
+ { name: "yarn", value: "yarn" },
21
+ { name: "bun", value: "bun" },
22
+ ],
23
+ default: config.packageManager || null,
24
+ },
25
+ {
26
+ type: "input",
27
+ name: "author",
28
+ message: "Default Author Name (for LICENSE):",
29
+ default: config.author || "The Authors",
30
+ filter: (v) => String(v || "").trim(),
31
+ },
32
+ {
33
+ type: "list",
34
+ name: "defaultLicense",
35
+ message: "Default License Type:",
36
+ choices: [
37
+ { name: "None", value: null },
38
+ inquirerSeparator(),
39
+ { name: "MIT", value: "MIT" },
40
+ { name: "Apache-2.0", value: "Apache2" },
41
+ { name: "ISC", value: "ISC" },
42
+ ],
43
+ default: config.defaultLicense || "MIT",
44
+ },
45
+ {
46
+ type: "confirm",
47
+ name: "learningMode",
48
+ message: "Enable Learning Mode by default?",
49
+ default: config.learningMode || false,
50
+ },
51
+ ]);
52
+
53
+ saveConfig(answers);
54
+ console.log(chalk.green("\nโœ“ Settings saved!"));
55
+ }
56
+
57
+ // Helper for pure inquirer usage if needed,
58
+ // though index.js passes the prompt instance.
59
+ function inquirerSeparator() {
60
+ return { name: "โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€", disabled: true };
61
+ }
62
+
63
+ module.exports = { runConfig };