@dawitworku/projectcli 0.1.1 → 0.1.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dawitworku/projectcli",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Interactive project generator (language -> framework -> create).",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",
@@ -26,7 +26,13 @@
26
26
  "lint": "node -c bin/projectcli.js && node -c src/index.js && node -c src/registry.js && node -c src/run.js"
27
27
  },
28
28
  "dependencies": {
29
- "inquirer": "^9.2.23",
30
- "inquirer-autocomplete-prompt": "^3.0.1"
29
+ "boxen": "^5.1.2",
30
+ "chalk": "^4.1.2",
31
+ "figlet": "^1.9.4",
32
+ "fuse.js": "^7.1.0",
33
+ "gradient-string": "^3.0.0",
34
+ "inquirer": "^8.2.6",
35
+ "inquirer-autocomplete-prompt": "^2.0.0",
36
+ "ora": "^5.4.1"
31
37
  }
32
38
  }
package/src/add.js CHANGED
@@ -1,5 +1,9 @@
1
1
  const fs = require("node:fs");
2
2
  const path = require("node:path");
3
+ const chalk = require("chalk");
4
+ const Fuse = require("fuse.js");
5
+ const inquirerImport = require("inquirer");
6
+ const inquirer = inquirerImport.default ?? inquirerImport;
3
7
 
4
8
  const { detectLanguage, detectPackageManager } = require("./detect");
5
9
  const { getCatalog } = require("./libraries");
@@ -21,18 +25,22 @@ function parseAddArgs(argv) {
21
25
  }
22
26
 
23
27
  function printStepsPreview(steps) {
24
- console.log("\nPlanned actions:");
28
+ console.log(chalk.bold.cyan("\nPlanned actions:"));
25
29
  for (const step of steps) {
26
30
  const type = step.type || "command";
27
31
  if (type === "command") {
28
32
  const where = step.cwdFromProjectRoot ? "(in project)" : "(here)";
29
- console.log(`- ${step.program} ${step.args.join(" ")} ${where}`);
33
+ console.log(
34
+ chalk.gray("- ") +
35
+ chalk.green(`${step.program} ${step.args.join(" ")}`) +
36
+ chalk.dim(` ${where}`)
37
+ );
30
38
  } else if (type === "mkdir") {
31
- console.log(`- mkdir -p ${step.path}`);
39
+ console.log(chalk.gray("- ") + chalk.yellow(`mkdir -p ${step.path}`));
32
40
  } else if (type === "writeFile") {
33
- console.log(`- write ${step.path}`);
41
+ console.log(chalk.gray("- ") + chalk.yellow(`write ${step.path}`));
34
42
  } else {
35
- console.log(`- ${type}`);
43
+ console.log(chalk.gray(`- ${type}`));
36
44
  }
37
45
  }
38
46
  console.log("");
@@ -42,7 +50,7 @@ async function runAdd({ prompt, argv }) {
42
50
  const flags = parseAddArgs(argv);
43
51
  const cwd = process.cwd();
44
52
  const language = detectLanguage(cwd);
45
- const pm = detectPackageManager(cwd);
53
+ const pm = detectPackageManager(cwd); // Now returns pip, poetry, cargo, go, etc.
46
54
 
47
55
  if (language === "Unknown") {
48
56
  throw new Error(
@@ -50,10 +58,8 @@ async function runAdd({ prompt, argv }) {
50
58
  );
51
59
  }
52
60
 
53
- console.log(`\nDetected project: ${language}`);
54
- if (language === "JavaScript/TypeScript") {
55
- console.log(`Detected package manager: ${pm}`);
56
- }
61
+ console.log(chalk.bold(`\nDetected project: ${chalk.blue(language)}`));
62
+ console.log(chalk.dim(`Detected package manager: ${pm}`));
57
63
 
58
64
  const catalog = getCatalog(language);
59
65
  const categories = Object.keys(catalog);
@@ -61,201 +67,180 @@ async function runAdd({ prompt, argv }) {
61
67
  throw new Error(`No library catalog configured for: ${language}`);
62
68
  }
63
69
 
70
+ // Flatten catalog for fuzzy search
71
+ const allItems = [];
72
+ for (const cat of categories) {
73
+ for (const item of catalog[cat]) {
74
+ allItems.push({
75
+ ...item,
76
+ category: cat,
77
+ displayName: `${cat}: ${item.label}`,
78
+ });
79
+ }
80
+ }
81
+
82
+ const fuse = new Fuse(allItems, {
83
+ keys: ["label", "category"],
84
+ threshold: 0.4,
85
+ });
86
+
64
87
  const BACK = "__back__";
65
88
  const EXIT = "__exit__";
66
89
 
67
- let selectedCategory = null;
68
- let selectedLabels = null;
90
+ let selectedItems = [];
69
91
 
70
92
  while (true) {
71
- if (!selectedCategory) {
72
- const categoryChoices = [
73
- ...categories.map((c) => ({
74
- name: `${c} (${catalog[c].length})`,
75
- value: c,
76
- })),
77
- new (require("inquirer").Separator)(),
78
- { name: "Exit", value: EXIT },
79
- ];
93
+ // If we have selected items, show them
94
+ if (selectedItems.length > 0) {
95
+ console.log(chalk.green(`\nSelected:`));
96
+ selectedItems.forEach((i) => console.log(` - ${i.displayName}`));
97
+ }
80
98
 
81
- const { category } = await prompt([
99
+ const choices = [
100
+ { name: "Done (Install selected)", value: "DONE" },
101
+ { name: "Search libraries (Fuzzy)", value: "SEARCH" },
102
+ { name: "Browse by Category", value: "BROWSE" },
103
+ new inquirer.Separator(),
104
+ { name: "Exit", value: EXIT },
105
+ ];
106
+
107
+ const { action } = await prompt([
108
+ {
109
+ type: "list",
110
+ name: "action",
111
+ message: "What do you want to do?",
112
+ choices,
113
+ pageSize: 10,
114
+ },
115
+ ]);
116
+
117
+ if (action === EXIT) return;
118
+
119
+ if (action === "SEARCH") {
120
+ // Fuzzy search
121
+ const { item } = await prompt([
82
122
  {
83
- type: "list",
84
- name: "category",
85
- message: "Category:",
86
- choices: categoryChoices,
87
- pageSize: 14,
123
+ type: "autocomplete",
124
+ name: "item",
125
+ message: "Search library:",
126
+ source: (answersSoFar, input) => {
127
+ if (!input)
128
+ return Promise.resolve(
129
+ allItems.map((i) => ({ name: i.displayName, value: i }))
130
+ );
131
+ return Promise.resolve(
132
+ fuse
133
+ .search(input)
134
+ .map((r) => ({ name: r.item.displayName, value: r.item }))
135
+ );
136
+ },
88
137
  },
89
138
  ]);
90
-
91
- if (category === EXIT) return;
92
- selectedCategory = category;
139
+ if (item) {
140
+ if (!selectedItems.find((i) => i.label === item.label)) {
141
+ selectedItems.push(item);
142
+ }
143
+ }
93
144
  continue;
94
145
  }
95
146
 
96
- if (!selectedLabels) {
97
- const items = catalog[selectedCategory] || [];
98
- const { picks } = await prompt([
147
+ if (action === "BROWSE") {
148
+ const { category } = await prompt([
99
149
  {
100
- type: "checkbox",
101
- name: "picks",
102
- message: "Select libraries (space to toggle):",
103
- choices: items.map((i) => ({ name: i.label, value: i.label })),
104
- validate: (v) =>
105
- v && v.length ? true : "Pick at least one library.",
106
- pageSize: 14,
150
+ type: "list",
151
+ name: "category",
152
+ message: "Category:",
153
+ choices: [...categories, { name: "Back", value: BACK }],
107
154
  },
108
155
  ]);
156
+ if (category === BACK) continue;
109
157
 
110
- selectedLabels = picks;
111
-
112
- const { next } = await prompt([
158
+ const items = catalog[category];
159
+ const { picked } = await prompt([
113
160
  {
114
- type: "list",
115
- name: "next",
116
- message: "Next:",
117
- choices: [
118
- { name: "Continue", value: "continue" },
119
- { name: "← Back", value: BACK },
120
- { name: "Cancel", value: EXIT },
121
- ],
122
- pageSize: 8,
161
+ type: "checkbox",
162
+ name: "picked",
163
+ message: "Select libraries:",
164
+ choices: items.map((i) => ({
165
+ name: i.label,
166
+ value: i,
167
+ checked: !!selectedItems.find((s) => s.label === i.label),
168
+ })),
123
169
  },
124
170
  ]);
125
171
 
126
- if (next === EXIT) return;
127
- if (next === BACK) {
128
- selectedLabels = null;
129
- selectedCategory = null;
130
- continue;
131
- }
132
-
133
- // continue
134
- }
135
-
136
- const items = catalog[selectedCategory] || [];
137
- const chosen = items.filter((i) => selectedLabels.includes(i.label));
138
-
139
- const packages = uniq(chosen.flatMap((i) => i.packages || []));
140
- const packagesDev = uniq(chosen.flatMap((i) => i.packagesDev || []));
141
- const posts = uniq(chosen.map((i) => i.post).filter(Boolean));
142
-
143
- const steps = [];
144
-
145
- if (language === "JavaScript/TypeScript") {
146
- if (!fs.existsSync(path.join(cwd, "package.json"))) {
147
- throw new Error(
148
- "No package.json found; run this inside a JS/TS project."
149
- );
150
- }
151
-
152
- if (packages.length) {
153
- const cmd = pmAddCommand(pm, packages, { dev: false });
154
- steps.push({ type: "command", ...cmd, cwdFromProjectRoot: true });
155
- }
156
- if (packagesDev.length) {
157
- const cmd = pmAddCommand(pm, packagesDev, { dev: true });
158
- steps.push({ type: "command", ...cmd, cwdFromProjectRoot: true });
159
- }
160
-
161
- // Optional post-steps
162
- for (const post of posts) {
163
- if (post === "tailwind-init") {
164
- const cmd = pmExecCommand(pm, "tailwindcss", ["init", "-p"]);
165
- steps.push({ type: "command", ...cmd, cwdFromProjectRoot: true });
166
- }
167
- if (post === "playwright-install") {
168
- const cmd = pmExecCommand(pm, "playwright", ["install"]);
169
- steps.push({ type: "command", ...cmd, cwdFromProjectRoot: true });
170
- }
171
- if (post === "shadcn-init") {
172
- // shadcn/ui uses a CLI; keep it interactive.
173
- const cmd = pmExecCommand(pm, "shadcn@latest", ["init"]);
174
- steps.push({ type: "command", ...cmd, cwdFromProjectRoot: true });
172
+ for (const p of picked) {
173
+ if (!selectedItems.find((i) => i.label === p.label)) {
174
+ selectedItems.push(p);
175
175
  }
176
176
  }
177
+ continue;
178
+ }
177
179
 
178
- if (flags.dryRun) {
179
- printStepsPreview(steps);
180
- console.log("Dry run: nothing executed.");
180
+ if (action === "DONE") {
181
+ if (selectedItems.length === 0) {
182
+ console.log(chalk.yellow("No libraries selected. Exiting."));
181
183
  return;
182
184
  }
183
-
184
- if (!flags.yes) {
185
- const { ok } = await prompt([
186
- {
187
- type: "confirm",
188
- name: "ok",
189
- message: "Install selected libraries now?",
190
- default: true,
191
- },
192
- ]);
193
- if (!ok) {
194
- selectedLabels = null;
195
- continue;
196
- }
197
- }
198
-
199
- await runSteps(steps, { projectRoot: cwd });
200
- console.log("\nDone. Libraries added.");
201
- return;
185
+ break;
202
186
  }
187
+ }
203
188
 
204
- if (language === "Python") {
205
- const all = uniq([...packages, ...packagesDev]);
206
- const { mode } = await prompt([
207
- {
208
- type: "list",
209
- name: "mode",
210
- message: "How to add Python packages?",
211
- choices: [
212
- {
213
- name: "Append to requirements.txt (no install)",
214
- value: "requirements",
215
- },
216
- {
217
- name: "pip install now (and append requirements.txt)",
218
- value: "pip",
219
- },
220
- ],
221
- default: "requirements",
222
- },
223
- ]);
189
+ // Install phase
190
+ const packages = uniq(selectedItems.flatMap((i) => i.packages || []));
191
+ const packagesDev = uniq(selectedItems.flatMap((i) => i.packagesDev || []));
192
+ const posts = uniq(selectedItems.map((i) => i.post).filter(Boolean));
224
193
 
225
- const reqPath = path.join(cwd, "requirements.txt");
226
- const toAppend = all.map((p) => `${p}\n`).join("");
194
+ const steps = [];
227
195
 
228
- if (flags.dryRun) {
229
- console.log("\nPlanned actions:");
230
- if (mode === "pip") {
231
- console.log(`- python -m pip install ${all.join(" ")} (here)`);
232
- }
233
- console.log(`- append requirements.txt: ${all.join(", ")}`);
234
- console.log("\nDry run: nothing executed.");
235
- return;
236
- }
196
+ // Generic handling based on 'pm'
197
+ if (packages.length) {
198
+ const cmd = pmAddCommand(pm, packages, { dev: false });
199
+ steps.push({ type: "command", ...cmd, cwdFromProjectRoot: true });
200
+ }
237
201
 
238
- if (mode === "pip") {
239
- await runSteps(
240
- [
241
- {
242
- type: "command",
243
- program: "python",
244
- args: ["-m", "pip", "install", ...all],
245
- cwdFromProjectRoot: true,
246
- },
247
- ],
248
- { projectRoot: cwd }
249
- );
250
- }
202
+ if (packagesDev.length) {
203
+ const cmd = pmAddCommand(pm, packagesDev, { dev: true });
204
+ steps.push({ type: "command", ...cmd, cwdFromProjectRoot: true });
205
+ }
251
206
 
252
- fs.appendFileSync(reqPath, toAppend, "utf8");
253
- console.log("\nDone. Updated requirements.txt");
254
- return;
207
+ // Post install steps
208
+ for (const post of posts) {
209
+ if (post === "tailwind-init") {
210
+ const cmd = pmExecCommand(pm, "tailwindcss", ["init", "-p"]);
211
+ steps.push({ type: "command", ...cmd, cwdFromProjectRoot: true });
212
+ }
213
+ if (post === "playwright-install") {
214
+ const cmd = pmExecCommand(pm, "playwright", ["install"]);
215
+ steps.push({ type: "command", ...cmd, cwdFromProjectRoot: true });
255
216
  }
217
+ if (post === "shadcn-init") {
218
+ const cmd = pmExecCommand(pm, "shadcn@latest", ["init"]);
219
+ steps.push({ type: "command", ...cmd, cwdFromProjectRoot: true });
220
+ }
221
+ }
256
222
 
257
- throw new Error(`Add mode not implemented yet for: ${language}`);
223
+ if (flags.dryRun) {
224
+ printStepsPreview(steps);
225
+ console.log("Dry run: nothing executed.");
226
+ return;
258
227
  }
228
+
229
+ if (!flags.yes) {
230
+ printStepsPreview(steps);
231
+ const { ok } = await prompt([
232
+ {
233
+ type: "confirm",
234
+ name: "ok",
235
+ message: "Proceed with installation?",
236
+ default: true,
237
+ },
238
+ ]);
239
+ if (!ok) return;
240
+ }
241
+
242
+ await runSteps(steps, { projectRoot: cwd });
243
+ console.log(chalk.green("\nDone. Libraries added."));
259
244
  }
260
245
 
261
246
  module.exports = {
package/src/detect.js CHANGED
@@ -6,11 +6,29 @@ function exists(cwd, rel) {
6
6
  }
7
7
 
8
8
  function detectPackageManager(cwd) {
9
+ // JS/TS
9
10
  if (exists(cwd, "pnpm-lock.yaml")) return "pnpm";
10
11
  if (exists(cwd, "yarn.lock")) return "yarn";
11
12
  if (exists(cwd, "bun.lockb") || exists(cwd, "bun.lock")) return "bun";
12
13
  if (exists(cwd, "package-lock.json")) return "npm";
13
- // fallback
14
+
15
+ // Python
16
+ if (exists(cwd, "poetry.lock")) return "poetry";
17
+ if (exists(cwd, "requirements.txt")) return "pip";
18
+ if (exists(cwd, "pyproject.toml")) return "pip"; // Default to pip/standard if no specific lock file
19
+
20
+ // Rust
21
+ if (exists(cwd, "Cargo.toml")) return "cargo";
22
+
23
+ // Go
24
+ if (exists(cwd, "go.mod")) return "go";
25
+
26
+ // PHP
27
+ if (exists(cwd, "composer.json")) return "composer";
28
+
29
+ // fallback for JS
30
+ if (exists(cwd, "package.json")) return "npm";
31
+
14
32
  return "npm";
15
33
  }
16
34
 
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 =
@@ -211,18 +215,22 @@ function withBack(choices) {
211
215
  }
212
216
 
213
217
  function printStepsPreview(steps) {
214
- console.log("\nPlanned actions:");
218
+ console.log(chalk.bold.cyan("\nPlanned actions:"));
215
219
  for (const step of steps) {
216
220
  const type = step.type || "command";
217
221
  if (type === "command") {
218
222
  const where = step.cwdFromProjectRoot ? "(in project)" : "(here)";
219
- 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
+ );
220
228
  } else if (type === "mkdir") {
221
- console.log(`- mkdir -p ${step.path}`);
229
+ console.log(chalk.gray("- ") + chalk.yellow(`mkdir -p ${step.path}`));
222
230
  } else if (type === "writeFile") {
223
- console.log(`- write ${step.path}`);
231
+ console.log(chalk.gray("- ") + chalk.yellow(`write ${step.path}`));
224
232
  } else {
225
- console.log(`- ${type}`);
233
+ console.log(chalk.gray(`- ${type}`));
226
234
  }
227
235
  }
228
236
  console.log("");
@@ -267,11 +275,22 @@ async function main(options = {}) {
267
275
  return;
268
276
  }
269
277
 
270
- console.log("\nprojectcli");
271
- console.log("Create a project in seconds.");
272
- console.log(`Host: ${os.platform()} ${os.arch()}`);
273
- console.log("Tip: use ↑/↓, Enter, and type-to-search when available.");
274
- 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
+ );
275
294
 
276
295
  const languages = getLanguages();
277
296
  if (languages.length === 0) {
@@ -319,15 +338,32 @@ async function main(options = {}) {
319
338
  return { name: `${lang} (${count})`, value: lang, short: lang };
320
339
  });
321
340
 
322
- const { language } = await prompt([
323
- {
324
- type: "list",
325
- name: "language",
326
- message: "Language:",
327
- choices: languageChoices,
328
- pageSize: 12,
329
- },
330
- ]);
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]);
331
367
 
332
368
  state.language = language;
333
369
  state.framework = undefined;
@@ -348,32 +384,32 @@ async function main(options = {}) {
348
384
  return { name: `${fw}${note}`, value: fw, short: fw };
349
385
  });
350
386
 
351
- const frameworkQuestion =
352
- hasAutocomplete && frameworkChoices.length > 12
353
- ? {
354
- type: "autocomplete",
355
- name: "framework",
356
- message: "Framework (type to search):",
357
- pageSize: 12,
358
- source: async (_answersSoFar, input) => {
359
- const q = String(input || "")
360
- .toLowerCase()
361
- .trim();
362
- if (!q) return withBack(frameworkChoices);
363
- return withBack(
364
- frameworkChoices.filter((c) =>
365
- String(c.name).toLowerCase().includes(q)
366
- )
367
- );
368
- },
369
- }
370
- : {
371
- type: "list",
372
- name: "framework",
373
- message: "Framework:",
374
- choices: withBack(frameworkChoices),
375
- pageSize: 12,
376
- };
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
+ };
377
413
 
378
414
  const answer = await prompt([frameworkQuestion]);
379
415
  if (answer.framework === BACK) {
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] };
package/src/registry.js CHANGED
@@ -94,6 +94,36 @@ const REGISTRY = {
94
94
  },
95
95
  notes: "Vite + Svelte (JS)",
96
96
  },
97
+ "Vite (Preact)": {
98
+ id: "js.vite.preact",
99
+ label: "Vite (Preact)",
100
+ commands: (ctx) => {
101
+ const projectName = getProjectName(ctx);
102
+ const pm = getPackageManager(ctx);
103
+ return [
104
+ pmExec(pm, "create-vite@latest", [
105
+ projectName,
106
+ "--template",
107
+ "preact",
108
+ ]),
109
+ pmInstall(pm, { cwdFromProjectRoot: true }),
110
+ ];
111
+ },
112
+ notes: "Vite + Preact (JS)",
113
+ },
114
+ "Vite (Lit)": {
115
+ id: "js.vite.lit",
116
+ label: "Vite (Lit)",
117
+ commands: (ctx) => {
118
+ const projectName = getProjectName(ctx);
119
+ const pm = getPackageManager(ctx);
120
+ return [
121
+ pmExec(pm, "create-vite@latest", [projectName, "--template", "lit"]),
122
+ pmInstall(pm, { cwdFromProjectRoot: true }),
123
+ ];
124
+ },
125
+ notes: "Vite + Lit (JS)",
126
+ },
97
127
  "Next.js": {
98
128
  id: "js.nextjs",
99
129
  label: "Next.js",
@@ -110,7 +140,6 @@ const REGISTRY = {
110
140
  commands: (ctx) => {
111
141
  const projectName = getProjectName(ctx);
112
142
  const pm = getPackageManager(ctx);
113
- // Astro docs recommend npm/pnpm/yarn create astro@latest; using the underlying package works too.
114
143
  return [pmExec(pm, "create-astro@latest", [projectName])];
115
144
  },
116
145
  notes: "Astro site (wizard)",
@@ -148,6 +177,63 @@ const REGISTRY = {
148
177
  },
149
178
  notes: "Nuxt via nuxi init",
150
179
  },
180
+ "Express (Generator)": {
181
+ id: "js.express",
182
+ label: "Express",
183
+ commands: (ctx) => {
184
+ const projectName = getProjectName(ctx);
185
+ const pm = getPackageManager(ctx);
186
+ return [
187
+ pmExec(pm, "express-generator", [projectName]),
188
+ pmInstall(pm, { cwdFromProjectRoot: true }),
189
+ ];
190
+ },
191
+ notes: "Scaffolds basic Express app",
192
+ },
193
+ "Fastify (CLI)": {
194
+ id: "js.fastify",
195
+ label: "Fastify",
196
+ commands: (ctx) => {
197
+ const projectName = getProjectName(ctx);
198
+ const pm = getPackageManager(ctx);
199
+ return [
200
+ pmExec(pm, "fastify-cli", ["generate", projectName]),
201
+ pmInstall(pm, { cwdFromProjectRoot: true }),
202
+ ];
203
+ },
204
+ notes: "Scaffolds Fastify app",
205
+ },
206
+ "React Native (Expo)": {
207
+ id: "js.expo",
208
+ label: "React Native (Expo)",
209
+ commands: (ctx) => {
210
+ const projectName = getProjectName(ctx);
211
+ const pm = getPackageManager(ctx);
212
+ // create-expo-app
213
+ return [pmExec(pm, "create-expo-app@latest", [projectName])];
214
+ },
215
+ notes: "Expo for React Native",
216
+ },
217
+ "Electron (Forge)": {
218
+ id: "js.electron",
219
+ label: "Electron (Forge)",
220
+ commands: (ctx) => {
221
+ const projectName = getProjectName(ctx);
222
+ const pm = getPackageManager(ctx);
223
+ return [pmExec(pm, "create-electron-app@latest", [projectName])];
224
+ },
225
+ notes: "Electron Forge template",
226
+ },
227
+ "Qwik City": {
228
+ id: "js.qwik",
229
+ label: "Qwik City",
230
+ commands: (ctx) => {
231
+ const projectName = getProjectName(ctx);
232
+ const pm = getPackageManager(ctx);
233
+ return [pmExec(pm, "create-qwik@latest", ["basic", projectName])];
234
+ },
235
+ notes: "Qwik framework",
236
+ },
151
237
  "Node.js (basic)": {
152
238
  id: "js.node.basic",
153
239
  label: "Node.js (basic)",
@@ -388,6 +474,29 @@ const REGISTRY = {
388
474
  ],
389
475
  notes: "Writes files only (no pip install).",
390
476
  },
477
+ Pyramid: {
478
+ id: "py.pyramid",
479
+ label: "Pyramid",
480
+ commands: (ctx) => {
481
+ const projectName = getProjectName(ctx);
482
+ return [
483
+ // Pyramid cookiecutter is common, but basic scaffold:
484
+ { type: "mkdir", path: "." },
485
+ {
486
+ type: "writeFile",
487
+ path: "requirements.txt",
488
+ content: "pyramid\nwaitress\n",
489
+ },
490
+ {
491
+ type: "writeFile",
492
+ path: "app.py",
493
+ 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",
495
+ },
496
+ ];
497
+ },
498
+ notes: "Pyramid basic app",
499
+ },
391
500
  },
392
501
 
393
502
  Rust: {
@@ -419,6 +528,35 @@ const REGISTRY = {
419
528
  },
420
529
  notes: "Uses cargo new --lib",
421
530
  },
531
+ "Tauri (App)": {
532
+ id: "rs.tauri",
533
+ label: "Tauri (App)",
534
+ commands: (ctx) => {
535
+ const projectName = getProjectName(ctx);
536
+ 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
+ return [pmExec(pm, "create-tauri-app@latest", [projectName])];
540
+ },
541
+ notes: "Cross-platform desktop app",
542
+ },
543
+ "Leptos (CSR)": {
544
+ id: "rs.leptos",
545
+ label: "Leptos (CSR)",
546
+ commands: (ctx) => {
547
+ const projectName = getProjectName(ctx);
548
+ return [
549
+ {
550
+ program: "cargo",
551
+ args: ["init", "--name", projectName],
552
+ cwdFromProjectRoot: true,
553
+ },
554
+ // In reality, one would use cargo-leptos or a template.
555
+ // Just a stub for now.
556
+ ];
557
+ },
558
+ notes: "Init basic Rust project (Leptos needs template usually)",
559
+ },
422
560
  },
423
561
 
424
562
  Go: {
@@ -459,6 +597,92 @@ const REGISTRY = {
459
597
  },
460
598
  notes: "Writes main.go + runs go mod init",
461
599
  },
600
+ "Buffalo (Web)": {
601
+ id: "go.buffalo",
602
+ label: "Buffalo (Web)",
603
+ commands: (ctx) => {
604
+ const projectName = getProjectName(ctx);
605
+ return [
606
+ {
607
+ program: "buffalo",
608
+ args: ["new", projectName],
609
+ },
610
+ ];
611
+ },
612
+ notes: "Requires 'buffalo' CLI installed",
613
+ },
614
+ "Fiber (Skeleton)": {
615
+ id: "go.fiber",
616
+ label: "Fiber (Skeleton)",
617
+ commands: (_ctx) => [
618
+ { type: "mkdir", path: "." },
619
+ {
620
+ type: "writeFile",
621
+ path: "main.go",
622
+ content: `package main
623
+
624
+ import "github.com/gofiber/fiber/v2"
625
+
626
+ func main() {
627
+ app := fiber.New()
628
+
629
+ app.Get("/", func(c *fiber.Ctx) error {
630
+ return c.SendString("Hello, World!")
631
+ })
632
+
633
+ app.Listen(":3000")
634
+ }
635
+ `,
636
+ },
637
+ {
638
+ program: "go",
639
+ args: ["mod", "init", "fiber-app"],
640
+ cwdFromProjectRoot: true,
641
+ },
642
+ {
643
+ program: "go",
644
+ args: ["get", "github.com/gofiber/fiber/v2"],
645
+ cwdFromProjectRoot: true,
646
+ },
647
+ ],
648
+ notes: "Basic Fiber setup",
649
+ },
650
+ },
651
+
652
+ "C++": {
653
+ "CMake (Basic)": {
654
+ id: "cpp.cmake",
655
+ label: "CMake (Basic)",
656
+ commands: (ctx) => {
657
+ const projectName = getProjectName(ctx);
658
+ return [
659
+ { type: "mkdir", path: "src" },
660
+ {
661
+ type: "writeFile",
662
+ path: "CMakeLists.txt",
663
+ content: `cmake_minimum_required(VERSION 3.10)
664
+ project(${projectName})
665
+
666
+ set(CMAKE_CXX_STANDARD 17)
667
+
668
+ add_executable(${projectName} src/main.cpp)
669
+ `,
670
+ },
671
+ {
672
+ type: "writeFile",
673
+ path: "src/main.cpp",
674
+ content: `#include <iostream>
675
+
676
+ int main() {
677
+ std::cout << "Hello, World!" << std::endl;
678
+ return 0;
679
+ }
680
+ `,
681
+ },
682
+ ];
683
+ },
684
+ notes: "Standard CMake structure",
685
+ },
462
686
  },
463
687
 
464
688
  "C#": {
@@ -491,6 +715,180 @@ const REGISTRY = {
491
715
  notes: "Requires dotnet SDK",
492
716
  },
493
717
  },
718
+
719
+ Ruby: {
720
+ Rails: {
721
+ id: "rb.rails",
722
+ label: "Rails",
723
+ commands: (ctx) => {
724
+ const projectName = getProjectName(ctx);
725
+ return [
726
+ {
727
+ program: "rails",
728
+ args: ["new", projectName],
729
+ },
730
+ ];
731
+ },
732
+ notes: "Requires rails gem installed",
733
+ },
734
+ "Bundler (gem)": {
735
+ id: "rb.bundler",
736
+ label: "Bundler (new gem)",
737
+ commands: (ctx) => {
738
+ const projectName = getProjectName(ctx);
739
+ return [
740
+ {
741
+ program: "bundle",
742
+ args: ["gem", projectName],
743
+ },
744
+ ];
745
+ },
746
+ notes: "Creates a new gem with bundler",
747
+ },
748
+ },
749
+
750
+ PHP: {
751
+ Laravel: {
752
+ id: "php.laravel",
753
+ label: "Laravel",
754
+ commands: (ctx) => {
755
+ const projectName = getProjectName(ctx);
756
+ return [
757
+ {
758
+ program: "composer",
759
+ args: ["create-project", "laravel/laravel", projectName],
760
+ },
761
+ ];
762
+ },
763
+ notes: "Uses composer create-project",
764
+ },
765
+ Symfony: {
766
+ id: "php.symfony",
767
+ label: "Symfony",
768
+ commands: (ctx) => {
769
+ const projectName = getProjectName(ctx);
770
+ return [
771
+ {
772
+ program: "composer",
773
+ args: ["create-project", "symfony/skeleton", projectName],
774
+ },
775
+ ];
776
+ },
777
+ notes: "Uses composer create-project",
778
+ },
779
+ },
780
+
781
+ "Java/Kotlin": {
782
+ "Spring Boot (Maven)": {
783
+ id: "java.springboot.maven",
784
+ label: "Spring Boot (Maven)",
785
+ commands: (ctx) => {
786
+ // Simple archetype or just hints. Spring Init is complex.
787
+ // Let's use maven quickstart for now as basic.
788
+ const projectName = getProjectName(ctx);
789
+ return [
790
+ {
791
+ program: "mvn",
792
+ args: [
793
+ "archetype:generate",
794
+ "-DgroupId=com.example",
795
+ `-DartifactId=${projectName}`,
796
+ "-DarchetypeArtifactId=maven-archetype-quickstart",
797
+ "-DinteractiveMode=false",
798
+ ],
799
+ },
800
+ ];
801
+ },
802
+ notes: "Basic Maven Quickstart",
803
+ },
804
+ "Gradle (Basic)": {
805
+ id: "java.gradle.basic",
806
+ label: "Gradle (Basic)",
807
+ commands: (ctx) => {
808
+ const projectName = getProjectName(ctx);
809
+ return [
810
+ { type: "mkdir", path: projectName },
811
+ {
812
+ program: "gradle",
813
+ args: [
814
+ "init",
815
+ "--type",
816
+ "basic",
817
+ "--dsl",
818
+ "kotlin",
819
+ "--project-name",
820
+ projectName,
821
+ ],
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.
827
+ },
828
+ ];
829
+ },
830
+ notes: "Gradle init",
831
+ },
832
+ Quarkus: {
833
+ id: "java.quarkus",
834
+ label: "Quarkus",
835
+ commands: (ctx) => {
836
+ const projectName = getProjectName(ctx);
837
+ return [
838
+ {
839
+ program: "mvn",
840
+ args: [
841
+ "io.quarkus.platform:quarkus-maven-plugin:create",
842
+ `-DprojectGroupId=org.acme`,
843
+ `-DprojectArtifactId=${projectName}`,
844
+ ],
845
+ },
846
+ ];
847
+ },
848
+ notes: "Quarkus app (Maven)",
849
+ },
850
+ },
851
+
852
+ Others: {
853
+ "HTML/CSS": {
854
+ id: "other.html",
855
+ label: "HTML/CSS (Vanilla)",
856
+ commands: (ctx) => {
857
+ return [
858
+ { type: "mkdir", path: "." },
859
+ {
860
+ type: "writeFile",
861
+ path: "index.html",
862
+ content:
863
+ '<!DOCTYPE html>\\n<html lang="en">\\n<head>\\n <meta charset="UTF-8">\\n <title>My App</title>\\n</head>\\n<body>\\n <h1>Hello World</h1>\\n</body>\\n</html>',
864
+ },
865
+ {
866
+ type: "writeFile",
867
+ path: "style.css",
868
+ content: "body { font-family: sans-serif; }",
869
+ },
870
+ ];
871
+ },
872
+ notes: "Just static files",
873
+ },
874
+ },
875
+
876
+ Dart: {
877
+ "Console Application": {
878
+ id: "dart.console",
879
+ label: "Console Application",
880
+ commands: (ctx) => {
881
+ const projectName = getProjectName(ctx);
882
+ return [
883
+ {
884
+ program: "dart",
885
+ args: ["create", "-t", "console", projectName],
886
+ },
887
+ ];
888
+ },
889
+ notes: "Requires Dart SDK",
890
+ },
891
+ },
494
892
  };
495
893
 
496
894
  function getLanguages() {