@dawitworku/projectcli 0.2.2 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dawitworku/projectcli",
3
- "version": "0.2.2",
3
+ "version": "3.0.0",
4
4
  "description": "The ultimate interactive project generator (language -> framework -> create).",
5
5
  "author": "Dawit Worku",
6
6
  "repository": {
@@ -13,13 +13,20 @@
13
13
  "homepage": "https://github.com/dawitworku/projectcli#readme",
14
14
  "license": "MIT",
15
15
  "type": "commonjs",
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
16
19
  "bin": {
17
20
  "projectcli": "bin/projectcli.js"
18
21
  },
19
22
  "files": [
20
23
  "bin",
24
+ "scripts",
21
25
  "src",
22
- "README.md"
26
+ "README.md",
27
+ "CHANGELOG.md",
28
+ "ROADMAP.md",
29
+ "docs"
23
30
  ],
24
31
  "engines": {
25
32
  "node": ">=18"
@@ -32,7 +39,10 @@
32
39
  ],
33
40
  "scripts": {
34
41
  "start": "node bin/projectcli.js",
35
- "lint": "node -c bin/projectcli.js && node -c src/index.js && node -c src/registry.js && node -c src/run.js && node -c src/settings.js && node -c src/config.js && node -c src/devcontainer.js && node -c src/license.js"
42
+ "test": "node --test",
43
+ "lint": "node -c bin/projectcli.js && node -c src/index.js && node -c src/registry_legacy.js && node -c src/run.js && node -c src/settings.js && node -c src/config.js && node -c src/devcontainer.js && node -c src/license.js",
44
+ "release:notes": "node scripts/release-notes.js $npm_package_version",
45
+ "release:check": "npm test && npm run lint && node -e \"const v=require('./package.json').version; if(!/^\\d+\\.\\d+\\.\\d+$/.test(v)) { console.error('Invalid semver: '+v); process.exit(1);} console.log('OK version', v)\""
36
46
  },
37
47
  "dependencies": {
38
48
  "boxen": "^5.1.2",
@@ -0,0 +1,34 @@
1
+ const fs = require("node:fs");
2
+ const path = require("node:path");
3
+
4
+ function readChangelog() {
5
+ const p = path.resolve(__dirname, "..", "CHANGELOG.md");
6
+ return fs.readFileSync(p, "utf8");
7
+ }
8
+
9
+ function extractSection(markdown, version) {
10
+ const header = `## ${version}`;
11
+ const idx = markdown.indexOf(header);
12
+ if (idx === -1) return null;
13
+ const rest = markdown.slice(idx);
14
+ const nextIdx = rest.indexOf("\n## ", header.length);
15
+ const section = nextIdx === -1 ? rest : rest.slice(0, nextIdx);
16
+ return section.trim() + "\n";
17
+ }
18
+
19
+ function main() {
20
+ const version = process.argv[2];
21
+ if (!version) {
22
+ console.error("Usage: node scripts/release-notes.js <version>");
23
+ process.exit(2);
24
+ }
25
+ const changelog = readChangelog();
26
+ const section = extractSection(changelog, version);
27
+ if (!section) {
28
+ console.error(`Could not find section '## ${version}' in CHANGELOG.md`);
29
+ process.exit(1);
30
+ }
31
+ process.stdout.write(section);
32
+ }
33
+
34
+ main();
package/src/add.js CHANGED
@@ -9,6 +9,9 @@ const { detectLanguage, detectPackageManager } = require("./detect");
9
9
  const { getCatalog } = require("./libraries");
10
10
  const { pmAddCommand, pmExecCommand } = require("./pm");
11
11
  const { runSteps } = require("./run");
12
+ const { generateCI, generateDocker } = require("./cicd");
13
+ const { generateDevContainer } = require("./devcontainer");
14
+ const { generateLicense, licenseTypes } = require("./license");
12
15
 
13
16
  function uniq(arr) {
14
17
  return Array.from(new Set(arr));
@@ -24,6 +27,53 @@ function parseAddArgs(argv) {
24
27
  return out;
25
28
  }
26
29
 
30
+ function filterExistingWriteFiles(steps, projectRoot) {
31
+ const kept = [];
32
+ const skipped = [];
33
+ for (const step of steps || []) {
34
+ if (step && step.type === "writeFile" && typeof step.path === "string") {
35
+ const target = path.resolve(projectRoot, step.path);
36
+ if (fs.existsSync(target)) {
37
+ skipped.push(step.path);
38
+ continue;
39
+ }
40
+ }
41
+ kept.push(step);
42
+ }
43
+ return { kept, skipped };
44
+ }
45
+
46
+ function getFirstPositional(argv) {
47
+ const args = Array.isArray(argv) ? argv : [];
48
+ for (const a of args) {
49
+ if (typeof a !== "string") continue;
50
+ if (a.startsWith("-")) continue;
51
+ return a;
52
+ }
53
+ return null;
54
+ }
55
+
56
+ function normalizeTemplateLanguage(language) {
57
+ if (language === "JavaScript/TypeScript") return "JavaScript";
58
+ if (language === "Java/Kotlin") return "Java";
59
+ return language;
60
+ }
61
+
62
+ function buildEslintFlatConfigCjs() {
63
+ return (
64
+ "module.exports = [\n" +
65
+ " {\n" +
66
+ ' files: ["**/*.js", "**/*.cjs", "**/*.mjs"],\n' +
67
+ " languageOptions: { ecmaVersion: 2022 },\n" +
68
+ " rules: {\n" +
69
+ ' "no-unused-vars": "warn",\n' +
70
+ ' "no-undef": "error",\n' +
71
+ " },\n" +
72
+ " },\n" +
73
+ "];\n"
74
+ );
75
+ }
76
+
27
77
  function printStepsPreview(steps) {
28
78
  console.log(chalk.bold.cyan("\nPlanned actions:"));
29
79
  for (const step of steps) {
@@ -46,12 +96,22 @@ function printStepsPreview(steps) {
46
96
  console.log("");
47
97
  }
48
98
 
49
- async function runAdd({ prompt, argv }) {
99
+ async function runAdd({ prompt, argv, effectiveConfig }) {
50
100
  const flags = parseAddArgs(argv);
51
101
  const cwd = process.cwd();
52
102
  const language = detectLanguage(cwd);
53
103
  const pm = detectPackageManager(cwd); // Now returns pip, poetry, cargo, go, etc.
54
104
 
105
+ const feature = String(getFirstPositional(argv) || "").toLowerCase();
106
+ const supportedFeatures = new Set([
107
+ "ci",
108
+ "docker",
109
+ "devcontainer",
110
+ "license",
111
+ "lint",
112
+ "test",
113
+ ]);
114
+
55
115
  if (language === "Unknown") {
56
116
  throw new Error(
57
117
  "Couldn't detect project type here. Run inside a project folder (package.json, pyproject.toml, go.mod, etc.)."
@@ -61,6 +121,109 @@ async function runAdd({ prompt, argv }) {
61
121
  console.log(chalk.bold(`\nDetected project: ${chalk.blue(language)}`));
62
122
  console.log(chalk.dim(`Detected package manager: ${pm}`));
63
123
 
124
+ if (feature && supportedFeatures.has(feature)) {
125
+ const langForTemplates = normalizeTemplateLanguage(language);
126
+ const cfg =
127
+ effectiveConfig && typeof effectiveConfig === "object"
128
+ ? effectiveConfig
129
+ : {};
130
+
131
+ const steps = [];
132
+
133
+ if (feature === "ci") {
134
+ steps.push(...generateCI(cwd, langForTemplates, pm));
135
+ } else if (feature === "docker") {
136
+ steps.push(...generateDocker(cwd, langForTemplates));
137
+ } else if (feature === "devcontainer") {
138
+ steps.push(...generateDevContainer(cwd, langForTemplates));
139
+ } else if (feature === "license") {
140
+ const type =
141
+ typeof cfg.license === "string" && licenseTypes.includes(cfg.license)
142
+ ? cfg.license
143
+ : typeof cfg.defaultLicense === "string" &&
144
+ licenseTypes.includes(cfg.defaultLicense)
145
+ ? cfg.defaultLicense
146
+ : "MIT";
147
+ const author =
148
+ typeof cfg.author === "string" && cfg.author.trim()
149
+ ? cfg.author.trim()
150
+ : "The Authors";
151
+
152
+ steps.push(...generateLicense(cwd, type, author));
153
+ } else if (feature === "lint") {
154
+ if (language === "JavaScript/TypeScript") {
155
+ const cmd = pmAddCommand(pm, ["eslint"], { dev: true });
156
+ steps.push({ type: "command", ...cmd, cwdFromProjectRoot: true });
157
+ steps.push({
158
+ type: "writeFile",
159
+ path: "eslint.config.cjs",
160
+ content: buildEslintFlatConfigCjs(),
161
+ });
162
+ } else if (language === "Python") {
163
+ const cmd = pmAddCommand(pm, ["ruff"], { dev: false });
164
+ steps.push({ type: "command", ...cmd, cwdFromProjectRoot: true });
165
+ } else {
166
+ console.log(
167
+ chalk.yellow(
168
+ `\nNo automated lint setup for ${language} yet. Try: projectcli add ci, docker, devcontainer, or license.`
169
+ )
170
+ );
171
+ return;
172
+ }
173
+ } else if (feature === "test") {
174
+ if (language === "JavaScript/TypeScript") {
175
+ const cmd = pmAddCommand(pm, ["vitest"], { dev: true });
176
+ steps.push({ type: "command", ...cmd, cwdFromProjectRoot: true });
177
+ } else if (language === "Python") {
178
+ const cmd = pmAddCommand(pm, ["pytest"], { dev: false });
179
+ steps.push({ type: "command", ...cmd, cwdFromProjectRoot: true });
180
+ } else {
181
+ console.log(
182
+ chalk.yellow(
183
+ `\nNo automated test setup for ${language} yet. Try: projectcli add ci, docker, devcontainer, or license.`
184
+ )
185
+ );
186
+ return;
187
+ }
188
+ }
189
+
190
+ const { kept, skipped } = filterExistingWriteFiles(steps, cwd);
191
+
192
+ if (flags.dryRun) {
193
+ printStepsPreview(kept);
194
+ if (skipped.length > 0) {
195
+ console.log(chalk.dim(`Skipped existing files: ${skipped.join(", ")}`));
196
+ }
197
+ console.log("Dry run: nothing executed.");
198
+ return;
199
+ }
200
+
201
+ if (!flags.yes) {
202
+ printStepsPreview(kept);
203
+ const { ok } = await prompt([
204
+ {
205
+ type: "confirm",
206
+ name: "ok",
207
+ message: `Proceed to add ${feature}?`,
208
+ default: true,
209
+ },
210
+ ]);
211
+ if (!ok) return;
212
+ }
213
+
214
+ if (kept.length === 0) {
215
+ console.log(chalk.dim("Nothing to do (already present)."));
216
+ return;
217
+ }
218
+
219
+ await runSteps(kept, { projectRoot: cwd });
220
+ if (skipped.length > 0) {
221
+ console.log(chalk.dim(`Skipped existing files: ${skipped.join(", ")}`));
222
+ }
223
+ console.log(chalk.green(`\nDone. Added ${feature}.`));
224
+ return;
225
+ }
226
+
64
227
  const catalog = getCatalog(language);
65
228
  const categories = Object.keys(catalog);
66
229
  if (categories.length === 0) {
package/src/config.js CHANGED
@@ -4,6 +4,70 @@ const os = require("node:os");
4
4
 
5
5
  const CONFIG_PATH = path.join(os.homedir(), ".projectcli.json");
6
6
 
7
+ const PROJECT_CONFIG_FILES = [".projectclirc", "projectcli.config.json"];
8
+
9
+ function safeReadJson(filePath) {
10
+ try {
11
+ if (!fs.existsSync(filePath)) return null;
12
+ const content = fs.readFileSync(filePath, "utf8");
13
+ return JSON.parse(content);
14
+ } catch {
15
+ return null;
16
+ }
17
+ }
18
+
19
+ function normalizeConfig(raw) {
20
+ if (!raw || typeof raw !== "object") return {};
21
+
22
+ const out = {};
23
+
24
+ if (typeof raw.packageManager === "string")
25
+ out.packageManager = raw.packageManager;
26
+ if (typeof raw.author === "string") out.author = raw.author;
27
+
28
+ // Accept either `license` (project-level) or `defaultLicense` (global config shape)
29
+ if (typeof raw.license === "string" || raw.license === null) {
30
+ out.license = raw.license;
31
+ }
32
+ if (typeof raw.defaultLicense === "string" || raw.defaultLicense === null) {
33
+ out.defaultLicense = raw.defaultLicense;
34
+ }
35
+
36
+ if (typeof raw.learningMode === "boolean")
37
+ out.learningMode = raw.learningMode;
38
+ if (typeof raw.learning === "boolean") out.learning = raw.learning;
39
+
40
+ if (typeof raw.preset === "string" || raw.preset === null) {
41
+ out.preset = raw.preset;
42
+ }
43
+
44
+ if (typeof raw.ci === "boolean") out.ci = raw.ci;
45
+ if (typeof raw.docker === "boolean") out.docker = raw.docker;
46
+ if (typeof raw.devcontainer === "boolean")
47
+ out.devcontainer = raw.devcontainer;
48
+
49
+ if (Array.isArray(raw.plugins)) {
50
+ out.plugins = raw.plugins.filter((p) => typeof p === "string" && p.trim());
51
+ }
52
+
53
+ return out;
54
+ }
55
+
56
+ function loadProjectConfig(cwd) {
57
+ const root = path.resolve(cwd || process.cwd());
58
+ for (const name of PROJECT_CONFIG_FILES) {
59
+ const candidate = path.resolve(root, name);
60
+ // Defensive: ensure we never read outside cwd.
61
+ const rel = path.relative(root, candidate);
62
+ if (rel.startsWith("..") || path.isAbsolute(rel)) continue;
63
+ const parsed = safeReadJson(candidate);
64
+ if (parsed && typeof parsed === "object") {
65
+ return { path: candidate, data: normalizeConfig(parsed) };
66
+ }
67
+ }
68
+ return { path: null, data: {} };
69
+ }
70
+
7
71
  function loadConfig() {
8
72
  try {
9
73
  if (fs.existsSync(CONFIG_PATH)) {
@@ -31,4 +95,6 @@ module.exports = {
31
95
  loadConfig,
32
96
  saveConfig,
33
97
  CONFIG_PATH,
98
+ loadProjectConfig,
99
+ PROJECT_CONFIG_FILES,
34
100
  };