@dawitworku/projectcli 0.1.0 → 0.1.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/src/index.js CHANGED
@@ -1,6 +1,10 @@
1
1
  const path = require("node:path");
2
2
  const fs = require("node:fs");
3
3
  const os = require("node:os");
4
+ const chalk = require("chalk");
5
+ const boxen = require("boxen");
6
+ const figlet = require("figlet");
7
+ const gradient = require("gradient-string");
4
8
  const inquirerImport = require("inquirer");
5
9
  const inquirer = inquirerImport.default ?? inquirerImport;
6
10
  const prompt =
@@ -25,6 +29,75 @@ const { getLanguages, getFrameworks, getGenerator } = require("./registry");
25
29
  const { runSteps } = require("./run");
26
30
  const { runAdd } = require("./add");
27
31
 
32
+ const RUST_KEYWORDS = new Set(
33
+ [
34
+ // Strict + reserved keywords (covers the common Cargo failure cases)
35
+ "as",
36
+ "break",
37
+ "const",
38
+ "continue",
39
+ "crate",
40
+ "else",
41
+ "enum",
42
+ "extern",
43
+ "false",
44
+ "fn",
45
+ "for",
46
+ "if",
47
+ "impl",
48
+ "in",
49
+ "let",
50
+ "loop",
51
+ "match",
52
+ "mod",
53
+ "move",
54
+ "mut",
55
+ "pub",
56
+ "ref",
57
+ "return",
58
+ "self",
59
+ "Self",
60
+ "static",
61
+ "struct",
62
+ "super",
63
+ "trait",
64
+ "true",
65
+ "type",
66
+ "unsafe",
67
+ "use",
68
+ "where",
69
+ "while",
70
+ "async",
71
+ "await",
72
+ "dyn",
73
+ "union",
74
+ // Reserved (historical / future)
75
+ "abstract",
76
+ "become",
77
+ "box",
78
+ "do",
79
+ "final",
80
+ "macro",
81
+ "override",
82
+ "priv",
83
+ "try",
84
+ "typeof",
85
+ "unsized",
86
+ "virtual",
87
+ "yield",
88
+ ].map(String)
89
+ );
90
+
91
+ function validateProjectNameForSelection({ language, framework }, name) {
92
+ if (language === "Rust" && typeof framework === "string") {
93
+ const usesCargo = framework.toLowerCase().includes("cargo");
94
+ if (usesCargo && RUST_KEYWORDS.has(name)) {
95
+ return `That name is a Rust keyword (${name}). Pick a different name.`;
96
+ }
97
+ }
98
+ return true;
99
+ }
100
+
28
101
  function isSafeProjectName(name) {
29
102
  // Avoid path traversal / empty names; keep permissive.
30
103
  if (!name) return false;
@@ -142,18 +215,22 @@ function withBack(choices) {
142
215
  }
143
216
 
144
217
  function printStepsPreview(steps) {
145
- console.log("\nPlanned actions:");
218
+ console.log(chalk.bold.cyan("\nPlanned actions:"));
146
219
  for (const step of steps) {
147
220
  const type = step.type || "command";
148
221
  if (type === "command") {
149
222
  const where = step.cwdFromProjectRoot ? "(in project)" : "(here)";
150
- console.log(`- ${step.program} ${step.args.join(" ")} ${where}`);
223
+ console.log(
224
+ chalk.gray("- ") +
225
+ chalk.green(`${step.program} ${step.args.join(" ")}`) +
226
+ chalk.dim(` ${where}`)
227
+ );
151
228
  } else if (type === "mkdir") {
152
- console.log(`- mkdir -p ${step.path}`);
229
+ console.log(chalk.gray("- ") + chalk.yellow(`mkdir -p ${step.path}`));
153
230
  } else if (type === "writeFile") {
154
- console.log(`- write ${step.path}`);
231
+ console.log(chalk.gray("- ") + chalk.yellow(`write ${step.path}`));
155
232
  } else {
156
- console.log(`- ${type}`);
233
+ console.log(chalk.gray(`- ${type}`));
157
234
  }
158
235
  }
159
236
  console.log("");
@@ -198,11 +275,22 @@ async function main(options = {}) {
198
275
  return;
199
276
  }
200
277
 
201
- console.log("\nprojectcli");
202
- console.log("Create a project in seconds.");
203
- console.log(`Host: ${os.platform()} ${os.arch()}`);
204
- console.log("Tip: use ↑/↓, Enter, and type-to-search when available.");
205
- console.log("Tip: Ctrl+C anytime to quit.\n");
278
+ // Clear console for a fresh start
279
+ console.clear();
280
+
281
+ const title = figlet.textSync(" PROJECT CLI", { font: "Slant" });
282
+ console.log(gradient.pastel.multiline(title));
283
+
284
+ const subtitle = " The Ultimate Interactive Project Generator ";
285
+ console.log(gradient.vice(subtitle));
286
+ console.log(chalk.dim(" v" + readPackageVersion()));
287
+ console.log("\n");
288
+
289
+ console.log(
290
+ chalk.cyan.bold(" ? ") +
291
+ chalk.bold("Select a Language") +
292
+ chalk.dim(" (Type to search)")
293
+ );
206
294
 
207
295
  const languages = getLanguages();
208
296
  if (languages.length === 0) {
@@ -250,15 +338,32 @@ async function main(options = {}) {
250
338
  return { name: `${lang} (${count})`, value: lang, short: lang };
251
339
  });
252
340
 
253
- const { language } = await prompt([
254
- {
255
- type: "list",
256
- name: "language",
257
- message: "Language:",
258
- choices: languageChoices,
259
- pageSize: 12,
260
- },
261
- ]);
341
+ const languageQuestion = hasAutocomplete
342
+ ? {
343
+ type: "autocomplete",
344
+ name: "language",
345
+ message: "Language (type to search):",
346
+ pageSize: 12,
347
+ source: async (_answersSoFar, input) => {
348
+ const q = String(input || "")
349
+ .toLowerCase()
350
+ .trim();
351
+ if (!q) return languageChoices;
352
+ // Fuzzy/Simple filter
353
+ return languageChoices.filter((c) =>
354
+ String(c.name).toLowerCase().includes(q)
355
+ );
356
+ },
357
+ }
358
+ : {
359
+ type: "list",
360
+ name: "language",
361
+ message: "Language:",
362
+ choices: languageChoices,
363
+ pageSize: 12,
364
+ };
365
+
366
+ const { language } = await prompt([languageQuestion]);
262
367
 
263
368
  state.language = language;
264
369
  state.framework = undefined;
@@ -279,32 +384,32 @@ async function main(options = {}) {
279
384
  return { name: `${fw}${note}`, value: fw, short: fw };
280
385
  });
281
386
 
282
- const frameworkQuestion =
283
- hasAutocomplete && frameworkChoices.length > 12
284
- ? {
285
- type: "autocomplete",
286
- name: "framework",
287
- message: "Framework (type to search):",
288
- pageSize: 12,
289
- source: async (_answersSoFar, input) => {
290
- const q = String(input || "")
291
- .toLowerCase()
292
- .trim();
293
- if (!q) return withBack(frameworkChoices);
294
- return withBack(
295
- frameworkChoices.filter((c) =>
296
- String(c.name).toLowerCase().includes(q)
297
- )
298
- );
299
- },
300
- }
301
- : {
302
- type: "list",
303
- name: "framework",
304
- message: "Framework:",
305
- choices: withBack(frameworkChoices),
306
- pageSize: 12,
307
- };
387
+ const frameworkQuestion = hasAutocomplete
388
+ ? {
389
+ type: "autocomplete",
390
+ name: "framework",
391
+ message: "Framework (type to search):",
392
+ pageSize: 12,
393
+ source: async (_answersSoFar, input) => {
394
+ const q = String(input || "")
395
+ .toLowerCase()
396
+ .trim();
397
+ const backOption = { name: "← Back", value: BACK };
398
+ if (!q) return [backOption, ...frameworkChoices];
399
+
400
+ const filtered = frameworkChoices.filter((c) =>
401
+ String(c.name).toLowerCase().includes(q)
402
+ );
403
+ return [backOption, ...filtered];
404
+ },
405
+ }
406
+ : {
407
+ type: "list",
408
+ name: "framework",
409
+ message: "Framework:",
410
+ choices: withBack(frameworkChoices),
411
+ pageSize: 12,
412
+ };
308
413
 
309
414
  const answer = await prompt([frameworkQuestion]);
310
415
  if (answer.framework === BACK) {
@@ -378,7 +483,10 @@ async function main(options = {}) {
378
483
  if (fs.existsSync(target)) {
379
484
  return "That folder already exists. Pick a different name.";
380
485
  }
381
- return true;
486
+ return validateProjectNameForSelection(
487
+ { language: state.language, framework: state.framework },
488
+ v
489
+ );
382
490
  },
383
491
  },
384
492
  ]);
@@ -452,6 +560,7 @@ async function main(options = {}) {
452
560
 
453
561
  if (action === "cancel") return;
454
562
  if (action === "back") {
563
+ state.name = undefined;
455
564
  step = "name";
456
565
  continue;
457
566
  }
@@ -471,6 +580,11 @@ async function main(options = {}) {
471
580
  const message = err && err.message ? err.message : String(err);
472
581
  console.error(`\nError: ${message}`);
473
582
 
583
+ const looksLikeNameIssue =
584
+ /cannot be used as a package name|Rust keyword|keyword|Cargo\.toml/i.test(
585
+ message
586
+ );
587
+
474
588
  if (args.yes) {
475
589
  throw err;
476
590
  }
@@ -481,22 +595,31 @@ async function main(options = {}) {
481
595
  name: "next",
482
596
  message: "What next?",
483
597
  choices: [
484
- { name: "Try again", value: "retry" },
598
+ { name: "Try again (same settings)", value: "retry" },
599
+ { name: "Change project name", value: "name" },
485
600
  { name: "← Back", value: "back" },
486
601
  { name: "Cancel", value: "cancel" },
487
602
  ],
488
603
  pageSize: 6,
604
+ default: looksLikeNameIssue ? "name" : "retry",
489
605
  },
490
606
  ]);
491
607
 
492
608
  if (next === "cancel") return;
609
+ if (next === "name") {
610
+ state.name = undefined;
611
+ step = "name";
612
+ continue;
613
+ }
493
614
  if (next === "back") {
615
+ state.name = undefined;
494
616
  step = "name";
495
617
  continue;
496
618
  }
497
619
 
498
620
  // retry
499
- step = "confirm";
621
+ step = looksLikeNameIssue ? "name" : "confirm";
622
+ if (step === "name") state.name = undefined;
500
623
  continue;
501
624
  }
502
625
 
package/src/libraries.js CHANGED
@@ -27,16 +27,21 @@ const JS_TS = {
27
27
  "UX / Animation": [
28
28
  { label: "Framer Motion", packages: ["framer-motion"] },
29
29
  { label: "Lottie", packages: ["lottie-react"] },
30
+ { label: "GSAP", packages: ["gsap"] },
31
+ { label: "Three.js", packages: ["three"] },
30
32
  ],
31
33
  "Forms / Validation": [
32
34
  { label: "React Hook Form", packages: ["react-hook-form"] },
33
35
  { label: "Zod", packages: ["zod"] },
34
36
  { label: "Yup", packages: ["yup"] },
37
+ { label: "Valibot", packages: ["valibot"] },
35
38
  ],
36
39
  "Data / State": [
37
40
  { label: "TanStack Query", packages: ["@tanstack/react-query"] },
38
41
  { label: "Zustand", packages: ["zustand"] },
39
42
  { label: "Redux Toolkit", packages: ["@reduxjs/toolkit", "react-redux"] },
43
+ { label: "Jotai", packages: ["jotai"] },
44
+ { label: "Recoil", packages: ["recoil"] },
40
45
  ],
41
46
  Testing: [
42
47
  { label: "Vitest", packagesDev: ["vitest"] },
@@ -45,6 +50,16 @@ const JS_TS = {
45
50
  packagesDev: ["@playwright/test"],
46
51
  post: "playwright-install",
47
52
  },
53
+ { label: "Jest", packagesDev: ["jest", "ts-jest", "@types/jest"] },
54
+ { label: "Cypress", packagesDev: ["cypress"] },
55
+ ],
56
+ "Backend / API": [
57
+ { label: "Axios", packages: ["axios"] },
58
+ {
59
+ label: "TRPC",
60
+ packages: ["@trpc/client", "@trpc/server", "@trpc/react-query"],
61
+ },
62
+ { label: "Socket.io Client", packages: ["socket.io-client"] },
48
63
  ],
49
64
  };
50
65
 
@@ -53,17 +68,80 @@ const PY = {
53
68
  { label: "Rich", packages: ["rich"] },
54
69
  { label: "Typer (CLI)", packages: ["typer"] },
55
70
  { label: "Textual", packages: ["textual"] },
71
+ { label: "Flet", packages: ["flet"] },
56
72
  ],
57
73
  Web: [
58
- { label: "FastAPI", packages: ["fastapi", "uvicorn"] },
74
+ { label: "FastAPI", packages: ["fastapi", "uvicorn[standard]"] },
59
75
  { label: "Flask", packages: ["flask"] },
60
76
  { label: "Django", packages: ["django"] },
77
+ { label: "Starlette", packages: ["starlette"] },
78
+ { label: "Litestar", packages: ["litestar"] },
79
+ ],
80
+ "Data Science": [
81
+ { label: "Pandas", packages: ["pandas"] },
82
+ { label: "NumPy", packages: ["numpy"] },
83
+ { label: "Matplotlib", packages: ["matplotlib"] },
84
+ { label: "Scikit-learn", packages: ["scikit-learn"] },
85
+ ],
86
+ Testing: [
87
+ { label: "Pytest", packages: ["pytest"] },
88
+ { label: "Unittest", packages: [] }, // builtin
89
+ ],
90
+ };
91
+
92
+ const RUST = {
93
+ Web: [
94
+ { label: "Axum", packages: ["axum", "tokio"] },
95
+ { label: "Actix-web", packages: ["actix-web"] },
96
+ { label: "Rocket", packages: ["rocket"] },
97
+ { label: "Yew", packages: ["yew"] },
98
+ { label: "Leptos", packages: ["leptos"] },
99
+ ],
100
+ "CLI / TUI": [
101
+ { label: "Clap", packages: ["clap"] },
102
+ { label: "Ratatui", packages: ["ratatui"] },
103
+ { label: "Dialoguer", packages: ["dialoguer"] },
104
+ { label: "Inquire", packages: ["inquire"] },
105
+ ],
106
+ "Async / Runtime": [{ label: "Tokio", packages: ["tokio"] }],
107
+ Serialization: [{ label: "Serde", packages: ["serde", "serde_json"] }],
108
+ "ORM / DB": [
109
+ { label: "Diesel", packages: ["diesel"] },
110
+ { label: "SQLx", packages: ["sqlx"] },
111
+ { label: "SeaORM", packages: ["sea-orm"] },
112
+ ],
113
+ Utilities: [
114
+ { label: "Anyhow", packages: ["anyhow"] },
115
+ { label: "Thiserror", packages: ["thiserror"] },
116
+ { label: "Log", packages: ["log"] },
117
+ { label: "Env_logger", packages: ["env_logger"] },
118
+ ],
119
+ };
120
+
121
+ const GO = {
122
+ Web: [
123
+ { label: "Gin", packages: ["github.com/gin-gonic/gin"] },
124
+ { label: "Echo", packages: ["github.com/labstack/echo/v4"] },
125
+ { label: "Fiber", packages: ["github.com/gofiber/fiber/v2"] },
126
+ { label: "Chi", packages: ["github.com/go-chi/chi/v5"] },
127
+ ],
128
+ CLI: [
129
+ { label: "Cobra", packages: ["github.com/spf13/cobra"] },
130
+ { label: "Viper", packages: ["github.com/spf13/viper"] },
131
+ { label: "Bubbletea", packages: ["github.com/charmbracelet/bubbletea"] },
132
+ ],
133
+ "ORM / DB": [
134
+ { label: "GORM", packages: ["gorm.io/gorm"] },
135
+ { label: "Ent", packages: ["entgo.io/ent"] },
136
+ { label: "Sqlc", packages: ["github.com/kyleconroy/sqlc/cmd/sqlc"] },
61
137
  ],
62
138
  };
63
139
 
64
140
  function getCatalog(language) {
65
141
  if (language === "JavaScript/TypeScript") return JS_TS;
66
142
  if (language === "Python") return PY;
143
+ if (language === "Rust") return RUST;
144
+ if (language === "Go") return GO;
67
145
  return {};
68
146
  }
69
147
 
package/src/pm.js CHANGED
@@ -2,6 +2,11 @@ function pmInstallCommand(pm) {
2
2
  if (pm === "pnpm") return { program: "pnpm", args: ["install"] };
3
3
  if (pm === "yarn") return { program: "yarn", args: ["install"] };
4
4
  if (pm === "bun") return { program: "bun", args: ["install"] };
5
+ if (pm === "poetry") return { program: "poetry", args: ["install"] };
6
+ if (pm === "pip")
7
+ return { program: "pip", args: ["install", "-r", "requirements.txt"] };
8
+ if (pm === "cargo") return { program: "cargo", args: ["build"] };
9
+ if (pm === "go") return { program: "go", args: ["mod", "tidy"] };
5
10
  return { program: "npm", args: ["install"] };
6
11
  }
7
12
 
@@ -23,6 +28,28 @@ function pmAddCommand(pm, packages, { dev = false } = {}) {
23
28
  return { program: "bun", args: ["add", ...(dev ? ["-d"] : []), ...pkgs] };
24
29
  }
25
30
 
31
+ // Python
32
+ if (pm === "pip") {
33
+ return { program: "pip", args: ["install", ...pkgs] };
34
+ }
35
+ if (pm === "poetry") {
36
+ return {
37
+ program: "poetry",
38
+ args: ["add", ...(dev ? ["--group", "dev"] : []), ...pkgs],
39
+ };
40
+ }
41
+
42
+ // Rust
43
+ if (pm === "cargo") {
44
+ return { program: "cargo", args: ["add", ...pkgs] };
45
+ }
46
+
47
+ // Go
48
+ if (pm === "go") {
49
+ // go get matches 'add' somewhat, but typically go get <pkg>
50
+ return { program: "go", args: ["get", ...pkgs] };
51
+ }
52
+
26
53
  return { program: "npm", args: ["install", ...(dev ? ["-D"] : []), ...pkgs] };
27
54
  }
28
55
 
@@ -32,6 +59,11 @@ function pmExecCommand(pm, pkg, pkgArgs) {
32
59
  if (pm === "pnpm") return { program: "pnpm", args: ["dlx", pkg, ...args] };
33
60
  if (pm === "yarn") return { program: "yarn", args: ["dlx", pkg, ...args] };
34
61
  if (pm === "bun") return { program: "bunx", args: [pkg, ...args] };
62
+ if (pm === "mvn")
63
+ return { program: "mvn", args: ["archetype:generate", ...args] };
64
+ if (pm === "gradle") return { program: "gradle", args: ["init", ...args] };
65
+ if (pm === "composer")
66
+ return { program: "composer", args: ["create-project", pkg, ...args] };
35
67
 
36
68
  // npm default
37
69
  return { program: "npx", args: ["--yes", pkg, ...args] };