@dawitworku/projectcli 0.1.4 โ†’ 0.2.1

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,110 @@
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
+
23
+ ## ๐Ÿš€ Quick Start
24
+
25
+ Run instantly with `npx`:
10
26
 
11
27
  ```bash
12
28
  npx @dawitworku/projectcli@latest
13
29
  ```
14
30
 
15
- You can also run non-interactively:
31
+ Or install globally:
16
32
 
17
33
  ```bash
18
- npx @dawitworku/projectcli@latest --language "TypeScript" --framework "NestJS" --name my-api --pm npm
34
+ npm install -g @dawitworku/projectcli
35
+ projectcli
19
36
  ```
20
37
 
21
- ## Run (dev)
38
+ ## ๐ŸŽฎ Interactive Mode
39
+
40
+ Just run `projectcli` and follow the prompts:
41
+
42
+ 1. Select **Language** (fuzzy search supported).
43
+ 2. Select **Framework** (React, Vue, Next.js, Actix, Axum, Django, FastAPI, etc.).
44
+ 3. Choose **Project Name**.
45
+ 4. (Optional) Add **CI/CD** or **Docker**.
46
+
47
+ ## ๐Ÿ›  Advanced Usage
48
+
49
+ ### Context Awareness
50
+
51
+ Run it inside a project to detect the language and offer relevant tools:
22
52
 
23
53
  ```bash
24
- npm install
25
- npm start
54
+ cd my-rust-project
55
+ projectcli
56
+ # Output: "โœ“ Detected active Rust project"
57
+ # Options: [Add GitHub Actions CI], [Add Dockerfile], [Add Dependencies]
26
58
  ```
27
59
 
28
- ## Install as a single-word command
60
+ ### Remote Templates
29
61
 
30
- From this repo folder:
62
+ Clone a starter kit from GitHub and make it your own instantly:
31
63
 
32
64
  ```bash
33
- npm install
34
- npm link
65
+ projectcli --template https://github.com/example/starter-repo --name my-app
35
66
  ```
36
67
 
37
- Then you can run:
68
+ ### Automation / CI Use
69
+
70
+ Skip the interactive prompts for scripts or specialized workflows:
38
71
 
39
72
  ```bash
40
- projectcli
73
+ projectcli --language Rust --framework Actix --name my-api --ci --docker --yes
41
74
  ```
42
75
 
43
- ## Add libraries to an existing project
76
+ ### Configuration
44
77
 
45
- Run inside a project folder:
78
+ Save your preferences (like default package manager):
46
79
 
47
80
  ```bash
48
- projectcli add
81
+ projectcli config
49
82
  ```
50
83
 
51
- ## Notes
84
+ ## ๐Ÿ“ฆ Supported Generators (Partial List)
52
85
 
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.
86
+ | Language | Frameworks / Tools |
87
+ | ------------------------- | ------------------------------------------------------------- |
88
+ | **JavaScript/TypeScript** | React (Vite), Vue, Next.js, NestJS, Express, Astro, Svelte... |
89
+ | **Rust** | Binary, Library, Actix Web, Axum, Rocket, Taurus... |
90
+ | **Python** | Poetry, Setuptools, Django, Flask, FastAPI... |
91
+ | **Go** | Binary, Fiber, Gin, Chi, Echo... |
92
+ | **PHP** | Laravel, Symfony, Slim... |
93
+ | **Java/Kotlin** | Spring Boot, Gradle/Maven... |
94
+ | **...and more** | C#, Ruby, Swift, Dart |
55
95
 
56
- ## Useful flags
96
+ ## ๐Ÿค Contributing
57
97
 
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
- ```
98
+ We love contributions! Whether it's adding a new framework to the registry or fixing a bug.
99
+
100
+ 1. Fork it.
101
+ 2. Create your feature branch (`git checkout -b feature/new-framework`).
102
+ 3. Commit your changes (`git commit -am 'Add support for X'`).
103
+ 4. Push to the branch (`git push origin feature/new-framework`).
104
+ 5. Create a new Pull Request.
67
105
 
68
- ## How it works
106
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for more details.
69
107
 
70
- - Choose language
71
- - Choose framework
72
- - Enter project name
73
- - CLI runs the underlying generator commands (npm/cargo/django-admin/etc.)
108
+ ## ๐Ÿ“ License
74
109
 
75
- If a generator command is missing on your machine, install it and re-run.
110
+ 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.1.4",
4
- "description": "Interactive project generator (language -> framework -> create).",
3
+ "version": "0.2.1",
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": {
package/src/cicd.js ADDED
@@ -0,0 +1,169 @@
1
+ const fs = require("node:fs");
2
+ const path = require("node:path");
3
+
4
+ const GITHUB_ACTIONS_JS = `name: CI
5
+ on: [push, pull_request]
6
+ jobs:
7
+ build:
8
+ runs-on: ubuntu-latest
9
+ steps:
10
+ - uses: actions/checkout@v4
11
+ - uses: actions/setup-node@v4
12
+ with:
13
+ node-version: 20
14
+ cache: 'npm'
15
+ - run: npm ci
16
+ - run: npm run build --if-present
17
+ - run: npm test --if-present
18
+ `;
19
+
20
+ const GITHUB_ACTIONS_PY = `name: CI
21
+ on: [push, pull_request]
22
+ jobs:
23
+ build:
24
+ runs-on: ubuntu-latest
25
+ steps:
26
+ - uses: actions/checkout@v4
27
+ - uses: actions/setup-python@v5
28
+ with:
29
+ python-version: '3.11'
30
+ cache: 'pip'
31
+ - run: pip install -r requirements.txt
32
+ `;
33
+
34
+ const GITHUB_ACTIONS_RUST = `name: CI
35
+ on: [push, pull_request]
36
+ jobs:
37
+ build:
38
+ runs-on: ubuntu-latest
39
+ steps:
40
+ - uses: actions/checkout@v4
41
+ - uses: dtolnay/rust-toolchain@stable
42
+ - run: cargo build --verbose
43
+ - run: cargo test --verbose
44
+ `;
45
+
46
+ const GITHUB_ACTIONS_GO = `name: CI
47
+ on: [push, pull_request]
48
+ jobs:
49
+ build:
50
+ runs-on: ubuntu-latest
51
+ steps:
52
+ - uses: actions/checkout@v4
53
+ - uses: actions/setup-go@v5
54
+ with:
55
+ go-version: '1.21'
56
+ - run: go build -v ./...
57
+ - run: go test -v ./...
58
+ `;
59
+
60
+ function getGitHubAction(language, pm) {
61
+ if (language === "JavaScript" || language === "TypeScript")
62
+ return GITHUB_ACTIONS_JS;
63
+ if (language === "Python") return GITHUB_ACTIONS_PY;
64
+ if (language === "Rust") return GITHUB_ACTIONS_RUST;
65
+ if (language === "Go") return GITHUB_ACTIONS_GO;
66
+ // Default generic
67
+ return `name: CI
68
+ on: [push, pull_request]
69
+ jobs:
70
+ build:
71
+ runs-on: ubuntu-latest
72
+ steps:
73
+ - uses: actions/checkout@v4
74
+ - run: echo "Add build steps here"
75
+ `;
76
+ }
77
+
78
+ const DOCKERFILE_JS = `FROM node:20-alpine AS base
79
+
80
+ FROM base AS deps
81
+ WORKDIR /app
82
+ COPY package.json ./
83
+ RUN npm install
84
+
85
+ FROM base AS runner
86
+ WORKDIR /app
87
+ COPY --from=deps /app/node_modules ./node_modules
88
+ COPY . .
89
+ # RUN npm run build
90
+ CMD ["node", "index.js"]
91
+ `;
92
+
93
+ const DOCKERFILE_PY = `FROM python:3.11-slim
94
+ WORKDIR /app
95
+ COPY requirements.txt .
96
+ RUN pip install -r requirements.txt
97
+ COPY . .
98
+ CMD ["python", "app.py"]
99
+ `;
100
+
101
+ const DOCKERFILE_GO = `FROM golang:1.21-alpine AS builder
102
+ WORKDIR /app
103
+ COPY . .
104
+ RUN go build -o myapp main.go
105
+
106
+ FROM alpine:latest
107
+ WORKDIR /app
108
+ COPY --from=builder /app/myapp .
109
+ CMD ["./myapp"]
110
+ `;
111
+
112
+ const DOCKERFILE_RUST = `FROM rust:1.75 as builder
113
+ WORKDIR /usr/src/myapp
114
+ COPY . .
115
+ RUN cargo install --path .
116
+
117
+ FROM debian:buster-slim
118
+ COPY --from=builder /usr/local/cargo/bin/myapp /usr/local/bin/myapp
119
+ CMD ["myapp"]
120
+ `;
121
+
122
+ function getDockerfile(language) {
123
+ if (language === "JavaScript" || language === "TypeScript")
124
+ return DOCKERFILE_JS;
125
+ if (language === "Python") return DOCKERFILE_PY;
126
+ if (language === "Go") return DOCKERFILE_GO;
127
+ if (language === "Rust") return DOCKERFILE_RUST;
128
+ return `# Dockerfile
129
+ FROM ubuntu:latest
130
+ WORKDIR /app
131
+ COPY . .
132
+ CMD ["echo", "Start..."]
133
+ `;
134
+ }
135
+
136
+ function generateCI(projectRoot, language, pm) {
137
+ const steps = [];
138
+
139
+ // GitHub Actions
140
+ const ghContent = getGitHubAction(language, pm);
141
+ steps.push({
142
+ type: "writeFile",
143
+ path: ".github/workflows/ci.yml",
144
+ content: ghContent,
145
+ });
146
+
147
+ return steps;
148
+ }
149
+
150
+ function generateDocker(projectRoot, language) {
151
+ const content = getDockerfile(language);
152
+ return [
153
+ {
154
+ type: "writeFile",
155
+ path: "Dockerfile",
156
+ content: content,
157
+ },
158
+ {
159
+ type: "writeFile",
160
+ path: ".dockerignore",
161
+ content: "node_modules\ntarget\n.venv\n.git\n",
162
+ },
163
+ ];
164
+ }
165
+
166
+ module.exports = {
167
+ generateCI,
168
+ generateDocker,
169
+ };
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,45 @@
1
+ const DESCRIPTIONS = {
2
+ // JavaScript
3
+ "js.vite.vanilla":
4
+ "Native ESM-based build tool. Best for: fast prototyping, zero-framework learning.",
5
+ "js.vite.react":
6
+ "Most popular UI library. Best for: interactive UIs, large ecosystem, jobs.",
7
+ "js.vite.vue":
8
+ "Progressive framework. Best for: easy learning curve, clean template syntax.",
9
+ "js.vite.svelte":
10
+ "Compiler-based framework. Best for: performance, less boilerplate, true reactivity.",
11
+ "js.nextjs":
12
+ "The React Framework for the Web. Best for: SEO, SSR/SSG, full-stack apps.",
13
+ "js.astro":
14
+ "Content-focused site builder. Best for: blogs, documentation, portfolios (ships less JS).",
15
+ "js.express":
16
+ "Minimalist web framework for Node.js. Best for: REST APIs, learning backend basics.",
17
+ "js.nestjs":
18
+ "Angular-style backend framework. Best for: enterprise, scalable architecture, TypeScript.",
19
+
20
+ // Python
21
+ "py.django":
22
+ "Batteries-included web framework. Best for: rapid dev, CMS, huge ecosystem.",
23
+ "py.flask.basic":
24
+ "Microframework. Best for: simple services, learning, flexibility.",
25
+ "py.fastapi.basic":
26
+ "Modern, fast (high-performance). Best for: APIs with auto-docs (Swagger), async support.",
27
+
28
+ // Rust
29
+ "rs.cargo.bin":
30
+ "Standard Rust binary. Best for: CLI tools, backend services, learning Rust.",
31
+ "rs.tauri":
32
+ "Build smaller, faster, and more secure desktop applications with a web frontend.",
33
+
34
+ // Go
35
+ "go.module.basic":
36
+ "Go's dependency management system. Best for: microservices, high concurrency.",
37
+ };
38
+
39
+ function getDescription(id) {
40
+ return DESCRIPTIONS[id] || "No description available.";
41
+ }
42
+
43
+ module.exports = {
44
+ getDescription,
45
+ };
package/src/index.js CHANGED
@@ -28,6 +28,13 @@ 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");
33
+ const { generateCI, generateDocker } = require("./cicd");
34
+ const { getDescription } = require("./descriptions");
35
+ const { detectLanguage, detectPackageManager } = require("./detect");
36
+ const { loadConfig } = require("./config");
37
+ const { runConfig } = require("./settings");
31
38
 
32
39
  const RUST_KEYWORDS = new Set(
33
40
  [
@@ -129,6 +136,10 @@ function parseArgs(argv) {
129
136
  framework: undefined,
130
137
  name: undefined,
131
138
  pm: undefined,
139
+ ci: false,
140
+ docker: false,
141
+ learning: false,
142
+ template: undefined,
132
143
  };
133
144
 
134
145
  const nextValue = (i) => {
@@ -143,7 +154,15 @@ function parseArgs(argv) {
143
154
  else if (a === "--list") out.list = true;
144
155
  else if (a === "--yes" || a === "-y") out.yes = true;
145
156
  else if (a === "--dry-run") out.dryRun = true;
146
- else if (a.startsWith("--language="))
157
+ else if (a === "--ci") out.ci = true;
158
+ else if (a === "--docker") out.docker = true;
159
+ else if (a === "--learning") out.learning = true;
160
+ else if (a.startsWith("--template="))
161
+ out.template = a.slice("--template=".length);
162
+ else if (a === "--template") {
163
+ out.template = nextValue(i);
164
+ i++;
165
+ } else if (a.startsWith("--language="))
147
166
  out.language = a.slice("--language=".length);
148
167
  else if (a === "--language") {
149
168
  out.language = nextValue(i);
@@ -189,6 +208,7 @@ function printHelp() {
189
208
  console.log(
190
209
  " projectcli --language <lang> --framework <fw> --name <project>"
191
210
  );
211
+ console.log(" projectcli config # configure defaults");
192
212
  console.log("");
193
213
  console.log("Flags:");
194
214
  console.log(" --help, -h Show help");
@@ -202,9 +222,14 @@ function printHelp() {
202
222
  console.log(
203
223
  " --pm Package manager for JS/TS (npm|pnpm|yarn|bun)"
204
224
  );
225
+ console.log(" --ci Auto-add GitHub Actions CI");
226
+ console.log(" --docker Auto-add Dockerfile");
227
+ console.log(" --learning Enable learning mode (shows descriptions)");
228
+ console.log(" --template Clone from a Git repository URL");
205
229
  }
206
230
 
207
231
  const BACK = "__back__";
232
+ const COMMUNITY = "Community / Remote";
208
233
 
209
234
  function withBack(choices) {
210
235
  return [
@@ -247,6 +272,18 @@ function printList() {
247
272
  }
248
273
  }
249
274
 
275
+ function showBanner() {
276
+ console.log("");
277
+ console.log(
278
+ gradient.pastel.multiline(
279
+ figlet.textSync("ProjectCLI", { font: "Standard" })
280
+ )
281
+ );
282
+ console.log(chalk.bold.magenta(" The Swiss Army Knife for Developers"));
283
+ console.log(chalk.dim(" v" + readPackageVersion()));
284
+ console.log("");
285
+ }
286
+
250
287
  async function main(options = {}) {
251
288
  if (!prompt) {
252
289
  throw new Error(
@@ -257,7 +294,19 @@ async function main(options = {}) {
257
294
  const argv = options.argv || [];
258
295
  const { cmd, rest } = splitCommand(argv);
259
296
  const args = parseArgs(rest);
297
+
298
+ if (
299
+ !args.list &&
300
+ !args.version &&
301
+ !args.help &&
302
+ rest.length === 0 &&
303
+ cmd === "init"
304
+ ) {
305
+ showBanner();
306
+ }
307
+
260
308
  if (args.help) {
309
+ showBanner();
261
310
  printHelp();
262
311
  return;
263
312
  }
@@ -275,6 +324,87 @@ async function main(options = {}) {
275
324
  return;
276
325
  }
277
326
 
327
+ if (cmd === "config") {
328
+ await runConfig({ prompt });
329
+ return;
330
+ }
331
+
332
+ // Smart Context Detection
333
+ if (
334
+ cmd === "init" &&
335
+ rest.length === 0 &&
336
+ !args.language &&
337
+ !args.framework &&
338
+ !args.template &&
339
+ !args.name
340
+ ) {
341
+ const detected = detectLanguage(process.cwd());
342
+ if (detected && detected !== "Unknown") {
343
+ console.clear();
344
+ console.log(
345
+ gradient.pastel.multiline(
346
+ figlet.textSync("ProjectCLI", { font: "Standard" })
347
+ )
348
+ );
349
+ console.log(chalk.bold.magenta(" The Swiss Army Knife for Developers"));
350
+ console.log("");
351
+ console.log(
352
+ chalk.green(`โœ“ Detected active ${chalk.bold(detected)} project.`)
353
+ );
354
+
355
+ const { action } = await prompt([
356
+ {
357
+ type: "list",
358
+ name: "action",
359
+ message: "What would you like to do?",
360
+ choices: [
361
+ { name: "Add Library / Dependency", value: "add" },
362
+ { name: "Add GitHub Actions CI", value: "ci" },
363
+ { name: "Add Dockerfile", value: "docker" },
364
+ new inquirer.Separator(),
365
+ { name: "Start New Project Here", value: "new" },
366
+ { name: "Exit", value: "exit" },
367
+ ],
368
+ },
369
+ ]);
370
+
371
+ if (action === "exit") process.exit(0);
372
+
373
+ if (action === "add") {
374
+ // reuse the add logic, passing empty args so it detects locally
375
+ await runAdd({ prompt, argv: [] });
376
+ return;
377
+ }
378
+
379
+ if (action === "ci" || action === "docker") {
380
+ const pm = detectPackageManager(process.cwd());
381
+ let langArg = detected;
382
+ if (detected === "JavaScript/TypeScript") langArg = "JavaScript";
383
+ if (detected === "Java/Kotlin") langArg = "Java";
384
+
385
+ const steps =
386
+ action === "ci"
387
+ ? generateCI(process.cwd(), langArg, pm)
388
+ : generateDocker(process.cwd(), langArg);
389
+
390
+ if (steps.length > 0) {
391
+ console.log("\nApplying changes...");
392
+ await runSteps(steps, { projectRoot: process.cwd() });
393
+ console.log(chalk.green("Done!"));
394
+ } else {
395
+ console.log(
396
+ chalk.yellow(
397
+ "No standard template available for this language yet."
398
+ )
399
+ );
400
+ }
401
+ return;
402
+ }
403
+
404
+ // If 'new', fall through to normal wizard
405
+ }
406
+ }
407
+
278
408
  // Clear console for a fresh start
279
409
  console.clear();
280
410
 
@@ -297,13 +427,26 @@ async function main(options = {}) {
297
427
  throw new Error("No languages configured.");
298
428
  }
299
429
 
430
+ const userConfig = loadConfig();
431
+
300
432
  const allowedPms = ["npm", "pnpm", "yarn", "bun"];
301
- const preselectedPm =
433
+ let preselectedPm =
302
434
  typeof args.pm === "string" && allowedPms.includes(args.pm)
303
435
  ? args.pm
304
436
  : undefined;
305
437
 
438
+ if (!preselectedPm && userConfig.packageManager) {
439
+ if (allowedPms.includes(userConfig.packageManager)) {
440
+ preselectedPm = userConfig.packageManager;
441
+ }
442
+ }
443
+
444
+ if (userConfig.learningMode && args.learning === false) {
445
+ args.learning = true;
446
+ }
447
+
306
448
  const state = {
449
+ template: args.template || undefined,
307
450
  language:
308
451
  args.language && languages.includes(args.language)
309
452
  ? args.language
@@ -325,7 +468,10 @@ async function main(options = {}) {
325
468
  state.language === "JavaScript" || state.language === "TypeScript";
326
469
 
327
470
  let step = "language";
328
- if (!state.language) step = "language";
471
+ if (state.template) {
472
+ if (!state.name) step = "name";
473
+ else step = "confirm";
474
+ } else if (!state.language) step = "language";
329
475
  else if (!state.framework) step = "framework";
330
476
  else if (needsPackageManager && !state.pm) step = "pm";
331
477
  else if (!state.name) step = "name";
@@ -418,6 +564,38 @@ async function main(options = {}) {
418
564
  }
419
565
 
420
566
  state.framework = answer.framework;
567
+
568
+ // Preflight Checks
569
+ const gen = getGenerator(state.language, state.framework);
570
+ if (gen && gen.check && gen.check.length > 0) {
571
+ /* eslint-disable-next-line no-console */
572
+ console.log(chalk.dim("\n(checking requirements...)"));
573
+ const results = await checkBinaries(gen.check);
574
+ const missing = results.filter((r) => !r.ok);
575
+
576
+ if (missing.length > 0) {
577
+ console.log(chalk.red.bold("\nMissing required tools:"));
578
+ missing.forEach((m) => console.log(chalk.red(` - ${m.bin}`)));
579
+ console.log(
580
+ chalk.yellow("You may not be able to build or run this project.\n")
581
+ );
582
+
583
+ const { proceed } = await prompt([
584
+ {
585
+ type: "confirm",
586
+ name: "proceed",
587
+ message: "Continue anyway?",
588
+ default: false,
589
+ },
590
+ ]);
591
+
592
+ if (!proceed) {
593
+ step = "framework";
594
+ continue;
595
+ }
596
+ }
597
+ }
598
+
421
599
  step = "pm";
422
600
  continue;
423
601
  }
@@ -493,6 +671,10 @@ async function main(options = {}) {
493
671
 
494
672
  const v = String(projectName || "").trim();
495
673
  if (v.toLowerCase() === "back") {
674
+ if (state.template) {
675
+ console.log("Operation cancelled.");
676
+ process.exit(0);
677
+ }
496
678
  step =
497
679
  state.language === "JavaScript" || state.language === "TypeScript"
498
680
  ? "pm"
@@ -506,13 +688,75 @@ async function main(options = {}) {
506
688
  }
507
689
 
508
690
  if (step === "confirm") {
691
+ const projectRoot = path.resolve(process.cwd(), state.name);
692
+ const targetExists = fs.existsSync(projectRoot);
693
+
694
+ if (state.template) {
695
+ if (targetExists && !args.dryRun) {
696
+ console.error(
697
+ `\nError: Target folder already exists: ${projectRoot}`
698
+ );
699
+ state.name = undefined;
700
+ step = "name";
701
+ continue;
702
+ }
703
+
704
+ console.log("\nProject Configuration:");
705
+ console.log(` Template: ${state.template}`);
706
+ console.log(` Folder: ${state.name}`);
707
+ console.log("");
708
+
709
+ if (!args.yes) {
710
+ const { action } = await prompt([
711
+ {
712
+ type: "list",
713
+ name: "action",
714
+ message: "Clone this template?",
715
+ choices: [
716
+ { name: "Clone template", value: "create" },
717
+ { name: "Cancel", value: "cancel" },
718
+ ],
719
+ },
720
+ ]);
721
+ if (action === "cancel") {
722
+ console.log("Aborted.");
723
+ return;
724
+ }
725
+ }
726
+
727
+ if (args.dryRun) {
728
+ console.log(
729
+ `[Dry Run] Would clone ${state.template} to ${projectRoot}`
730
+ );
731
+ return;
732
+ }
733
+
734
+ console.log(chalk.dim("\nCloning repository..."));
735
+ try {
736
+ await gitClone(state.template, projectRoot);
737
+ // Remove .git to make it a fresh project
738
+ removeGitFolder(projectRoot);
739
+
740
+ console.log(
741
+ chalk.green(`\nSuccess! Created project at ${projectRoot}`)
742
+ );
743
+ console.log(
744
+ chalk.dim(
745
+ "You may need to run 'npm install' or similar inside the folder."
746
+ )
747
+ );
748
+ } catch (err) {
749
+ console.error(chalk.red("\nFailed to clone template:"), err.message);
750
+ process.exit(1);
751
+ }
752
+ return;
753
+ }
754
+
509
755
  const generator = getGenerator(state.language, state.framework);
510
756
  if (!generator) {
511
757
  throw new Error("Generator not found (registry mismatch).");
512
758
  }
513
759
 
514
- const projectRoot = path.resolve(process.cwd(), state.name);
515
- const targetExists = fs.existsSync(projectRoot);
516
760
  if (targetExists && !args.dryRun) {
517
761
  console.error(`\nError: Target folder already exists: ${projectRoot}`);
518
762
  state.name = undefined;
@@ -526,6 +770,20 @@ async function main(options = {}) {
526
770
  `Framework: ${state.framework}`,
527
771
  ];
528
772
  if (state.pm) summaryLines.push(`Package manager: ${state.pm}`);
773
+
774
+ if (args.learning) {
775
+ const desc = getDescription(generator.id);
776
+ console.log(
777
+ chalk.cyan(
778
+ boxen(desc, {
779
+ padding: 1,
780
+ title: `About ${state.framework}`,
781
+ borderStyle: "round",
782
+ })
783
+ )
784
+ );
785
+ }
786
+
529
787
  console.log("\n" + summaryLines.join("\n") + "\n");
530
788
 
531
789
  if (targetExists && args.dryRun) {
@@ -623,6 +881,83 @@ async function main(options = {}) {
623
881
  continue;
624
882
  }
625
883
 
884
+ // Git init
885
+ if (!args.dryRun) {
886
+ let doGit = args.yes;
887
+ if (!doGit) {
888
+ const { git } = await prompt([
889
+ {
890
+ type: "confirm",
891
+ name: "git",
892
+ message: "Initialize a new git repository?",
893
+ default: true,
894
+ },
895
+ ]);
896
+ doGit = git;
897
+ }
898
+
899
+ if (doGit) {
900
+ try {
901
+ await runSteps(
902
+ [
903
+ { program: "git", args: ["init"], cwdFromProjectRoot: true },
904
+ {
905
+ program: "git",
906
+ args: ["add", "."],
907
+ cwdFromProjectRoot: true,
908
+ },
909
+ {
910
+ program: "git",
911
+ args: ["commit", "-m", "Initial commit"],
912
+ cwdFromProjectRoot: true,
913
+ },
914
+ ],
915
+ { projectRoot }
916
+ );
917
+ console.log("Initialized git repository.");
918
+ } catch (e) {
919
+ console.warn("Git init failed (is git installed?):", e.message);
920
+ }
921
+ }
922
+ }
923
+
924
+ // CI/CD & Docker
925
+ if (!args.dryRun) {
926
+ let wantCi = args.ci;
927
+ let wantDocker = args.docker;
928
+
929
+ if (!args.yes) {
930
+ const { extras } = await prompt([
931
+ {
932
+ type: "checkbox",
933
+ name: "extras",
934
+ message: "Extras:",
935
+ choices: [
936
+ { name: "GitHub Actions CI", value: "ci", checked: wantCi },
937
+ { name: "Dockerfile", value: "docker", checked: wantDocker },
938
+ ],
939
+ },
940
+ ]);
941
+ if (extras) {
942
+ wantCi = extras.includes("ci");
943
+ wantDocker = extras.includes("docker");
944
+ }
945
+ }
946
+
947
+ const extraSteps = [];
948
+ if (wantCi) {
949
+ extraSteps.push(...generateCI(projectRoot, state.language, state.pm));
950
+ }
951
+ if (wantDocker) {
952
+ extraSteps.push(...generateDocker(projectRoot, state.language));
953
+ }
954
+
955
+ if (extraSteps.length > 0) {
956
+ console.log("Adding extras...");
957
+ await runSteps(extraSteps, { projectRoot });
958
+ }
959
+ }
960
+
626
961
  console.log(`\nDone. Created project in: ${projectRoot}`);
627
962
  return;
628
963
  }
package/src/libraries.js CHANGED
@@ -137,11 +137,60 @@ const GO = {
137
137
  ],
138
138
  };
139
139
 
140
+ const PHP = {
141
+ "Web Frameworks": [
142
+ { label: "Laravel", packages: ["laravel/framework"] },
143
+ { label: "Symfony", packages: ["symfony/symfony"] },
144
+ { label: "Slim", packages: ["slim/slim"] },
145
+ ],
146
+ Testing: [
147
+ { label: "PHPUnit", packages: [], packagesDev: ["phpunit/phpunit"] },
148
+ { label: "Pest", packages: [], packagesDev: ["pestphp/pest"] },
149
+ ],
150
+ Tools: [
151
+ {
152
+ label: "PHPStudio (CS Fixer)",
153
+ packages: [],
154
+ packagesDev: ["friendsofphp/php-cs-fixer"],
155
+ },
156
+ { label: "Monolog", packages: ["monolog/monolog"] },
157
+ ],
158
+ };
159
+
160
+ const RUBY = {
161
+ Web: [
162
+ { label: "Rails", packages: ["rails"] },
163
+ { label: "Sinatra", packages: ["sinatra"] },
164
+ ],
165
+ Testing: [{ label: "RSpec", packages: ["rspec"] }],
166
+ Tools: [{ label: "Rubocop", packages: ["rubocop"] }],
167
+ };
168
+
169
+ const DART = {
170
+ "Web/App": [{ label: "Shelf (Server)", packages: ["shelf", "shelf_router"] }],
171
+ State: [
172
+ { label: "Provider", packages: ["provider"] },
173
+ { label: "Riverpod", packages: ["flutter_riverpod"] },
174
+ { label: "Bloc", packages: ["flutter_bloc"] },
175
+ ],
176
+ Utils: [
177
+ { label: "Dio (HTTP)", packages: ["dio"] },
178
+ {
179
+ label: "Freezed",
180
+ packages: ["freezed_annotation"],
181
+ packagesDev: ["build_runner", "freezed"],
182
+ },
183
+ ],
184
+ };
185
+
140
186
  function getCatalog(language) {
141
187
  if (language === "JavaScript/TypeScript") return JS_TS;
142
188
  if (language === "Python") return PY;
143
189
  if (language === "Rust") return RUST;
144
190
  if (language === "Go") return GO;
191
+ if (language === "PHP") return PHP;
192
+ if (language === "Ruby") return RUBY;
193
+ if (language === "Dart") return DART;
145
194
  return {};
146
195
  }
147
196
 
package/src/pm.js CHANGED
@@ -50,6 +50,33 @@ function pmAddCommand(pm, packages, { dev = false } = {}) {
50
50
  return { program: "go", args: ["get", ...pkgs] };
51
51
  }
52
52
 
53
+ // PHP
54
+ if (pm === "composer") {
55
+ return {
56
+ program: "composer",
57
+ args: ["require", ...(dev ? ["--dev"] : []), ...pkgs],
58
+ };
59
+ }
60
+
61
+ // Ruby
62
+ if (pm === "bundle") {
63
+ // bundle add pkg
64
+ // dev dependencies often go to "development" or "test" group, but let's just do default add for now
65
+ // or standard "bundle add generic"
66
+ return {
67
+ program: "bundle",
68
+ args: ["add", ...pkgs, ...(dev ? ["--group", "development,test"] : [])],
69
+ };
70
+ }
71
+
72
+ // Dart
73
+ if (pm === "dart") {
74
+ return {
75
+ program: "dart",
76
+ args: ["pub", "add", ...(dev ? ["--dev"] : []), ...pkgs],
77
+ };
78
+ }
79
+
53
80
  return { program: "npm", args: ["install", ...(dev ? ["-D"] : []), ...pkgs] };
54
81
  }
55
82
 
@@ -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,23 +425,24 @@ 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
  {
429
432
  type: "writeFile",
430
433
  path: "app.py",
431
434
  content:
432
- "from flask import Flask\n\napp = Flask(__name__)\n\n@app.get('/')\ndef hello():\n return {'status': 'ok'}\n\nif __name__ == '__main__':\n app.run(debug=True)\n",
435
+ "from flask import Flask\\n\\napp = Flask(__name__)\\n\\n@app.get('/')\\ndef hello():\\n return {'status': 'ok'}\\n\\nif __name__ == '__main__':\\n app.run(debug=True)\\n",
433
436
  },
434
437
  {
435
438
  type: "writeFile",
436
439
  path: "requirements.txt",
437
- content: "flask\n",
440
+ content: "flask\\n",
438
441
  },
439
442
  {
440
443
  type: "writeFile",
441
444
  path: ".gitignore",
442
- content: ".venv\n__pycache__\n*.pyc\n.DS_Store\n",
445
+ content: ".venv\\n__pycache__\\n*.pyc\\n.DS_Store\\n",
443
446
  },
444
447
  ],
445
448
  notes: "Writes app.py + requirements.txt (no pip install).",
@@ -453,23 +456,23 @@ const REGISTRY = {
453
456
  type: "writeFile",
454
457
  path: "main.py",
455
458
  content:
456
- "from fastapi import FastAPI\n\napp = FastAPI()\n\n@app.get('/')\ndef root():\n return {'status': 'ok'}\n",
459
+ "from fastapi import FastAPI\\n\\napp = FastAPI()\\n\\n@app.get('/')\\ndef root():\\n return {'status': 'ok'}\\n",
457
460
  },
458
461
  {
459
462
  type: "writeFile",
460
463
  path: "requirements.txt",
461
- content: "fastapi\nuvicorn\n",
464
+ content: "fastapi\\nuvicorn\\n",
462
465
  },
463
466
  {
464
467
  type: "writeFile",
465
468
  path: "README.md",
466
469
  content:
467
- "# FastAPI app\n\nRun:\n\n- python -m venv .venv\n- . .venv/bin/activate\n- pip install -r requirements.txt\n- uvicorn main:app --reload\n",
470
+ "# FastAPI app\\n\\nRun:\\n\\n- python -m venv .venv\\n- . .venv/bin/activate\\n- pip install -r requirements.txt\\n- uvicorn main:app --reload\\n",
468
471
  },
469
472
  {
470
473
  type: "writeFile",
471
474
  path: ".gitignore",
472
- content: ".venv\n__pycache__\n*.pyc\n.DS_Store\n",
475
+ content: ".venv\\n__pycache__\\n*.pyc\\n.DS_Store\\n",
473
476
  },
474
477
  ],
475
478
  notes: "Writes files only (no pip install).",
@@ -480,18 +483,17 @@ const REGISTRY = {
480
483
  commands: (ctx) => {
481
484
  const projectName = getProjectName(ctx);
482
485
  return [
483
- // Pyramid cookiecutter is common, but basic scaffold:
484
486
  { type: "mkdir", path: "." },
485
487
  {
486
488
  type: "writeFile",
487
489
  path: "requirements.txt",
488
- content: "pyramid\nwaitress\n",
490
+ content: "pyramid\\nwaitress\\n",
489
491
  },
490
492
  {
491
493
  type: "writeFile",
492
494
  path: "app.py",
493
495
  content:
494
- "from wsgiref.simple_server import make_server\nfrom pyramid.config import Configurator\nfrom pyramid.response import Response\n\ndef hello_world(request):\n return Response('Hello World!')\n\nif __name__ == '__main__':\n with Configurator() as config:\n config.add_route('hello', '/')\n config.add_view(hello_world, route_name='hello')\n app = config.make_wsgi_app()\n server = make_server('0.0.0.0', 6543, app)\n server.serve_forever()\n",
496
+ "from wsgiref.simple_server import make_server\\nfrom pyramid.config import Configurator\\nfrom pyramid.response import Response\\n\\ndef hello_world(request):\\n return Response('Hello World!')\\n\\nif __name__ == '__main__':\\n with Configurator() as config:\\n config.add_route('hello', '/')\\n config.add_view(hello_world, route_name='hello')\\n app = config.make_wsgi_app()\\n server = make_server('0.0.0.0', 6543, app)\\n server.serve_forever()\\n",
495
497
  },
496
498
  ];
497
499
  },
@@ -503,6 +505,7 @@ const REGISTRY = {
503
505
  "Cargo (bin)": {
504
506
  id: "rs.cargo.bin",
505
507
  label: "Cargo (bin)",
508
+ check: ["cargo"],
506
509
  commands: (ctx) => {
507
510
  const projectName = getProjectName(ctx);
508
511
  return [
@@ -517,6 +520,7 @@ const REGISTRY = {
517
520
  "Cargo (lib)": {
518
521
  id: "rs.cargo.lib",
519
522
  label: "Cargo (lib)",
523
+ check: ["cargo"],
520
524
  commands: (ctx) => {
521
525
  const projectName = getProjectName(ctx);
522
526
  return [
@@ -534,8 +538,6 @@ const REGISTRY = {
534
538
  commands: (ctx) => {
535
539
  const projectName = getProjectName(ctx);
536
540
  const pm = getPackageManager(ctx); // usually npm for tauri frontend
537
- // Using create-tauri-app via pmExec
538
- // cargo create-tauri-app is also an option but node way is common if JS frontend
539
541
  return [pmExec(pm, "create-tauri-app@latest", [projectName])];
540
542
  },
541
543
  notes: "Cross-platform desktop app",
@@ -551,8 +553,6 @@ const REGISTRY = {
551
553
  args: ["init", "--name", projectName],
552
554
  cwdFromProjectRoot: true,
553
555
  },
554
- // In reality, one would use cargo-leptos or a template.
555
- // Just a stub for now.
556
556
  ];
557
557
  },
558
558
  notes: "Init basic Rust project (Leptos needs template usually)",
@@ -563,6 +563,7 @@ const REGISTRY = {
563
563
  "Go module (basic)": {
564
564
  id: "go.module.basic",
565
565
  label: "Go module (basic)",
566
+ check: ["go"],
566
567
  commands: (ctx) => {
567
568
  const projectName = getProjectName(ctx);
568
569
  return [
@@ -591,7 +592,7 @@ const REGISTRY = {
591
592
  type: "writeFile",
592
593
  path: "main.go",
593
594
  content:
594
- 'package main\n\nimport "fmt"\n\nfunc main() {\n fmt.Println("hello")\n}\n',
595
+ 'package main\\n\\nimport "fmt"\\n\\nfunc main() {\\n fmt.Println("hello")\\n}\\n',
595
596
  },
596
597
  ];
597
598
  },
@@ -783,8 +784,6 @@ int main() {
783
784
  id: "java.springboot.maven",
784
785
  label: "Spring Boot (Maven)",
785
786
  commands: (ctx) => {
786
- // Simple archetype or just hints. Spring Init is complex.
787
- // Let's use maven quickstart for now as basic.
788
787
  const projectName = getProjectName(ctx);
789
788
  return [
790
789
  {
@@ -819,11 +818,8 @@ int main() {
819
818
  "--project-name",
820
819
  projectName,
821
820
  ],
822
- cwd: projectName, // This property needs to be supported in runSteps, handled via chdir logic usually?
823
- // current runSteps implementation supports cwdFromProjectRoot but not arbitrary cwd change easily maybe?
824
- // runSteps implementation check:
825
- // It uses `cwdFromProjectRoot` or defaults to current process.cwd().
826
- // Better to just run gradle init inside the folder.
821
+ cwd: projectName,
822
+ cwdFromProjectRoot: true,
827
823
  },
828
824
  ];
829
825
  },
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,45 @@
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
+ new 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: "confirm",
27
+ name: "learningMode",
28
+ message: "Enable Learning Mode by default?",
29
+ default: config.learningMode || false,
30
+ },
31
+ ]);
32
+
33
+ saveConfig(answers);
34
+ console.log(chalk.green("\nโœ“ Settings saved!"));
35
+ }
36
+
37
+ // Helper for pure inquirer usage if needed,
38
+ // though index.js passes the prompt instance.
39
+ function inquirerSeparator() {
40
+ // We can't easily import inquirer.Separator here without adding dependency
41
+ // or passing it in. Let's just use a string for now or skip it.
42
+ return { name: "โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€", disabled: true };
43
+ }
44
+
45
+ module.exports = { runConfig };