Skip to content

Commit 21fbb41

Browse files
committed
Fix backend flow, installer edge cases, and generation progress
1 parent d7aec17 commit 21fbb41

File tree

16 files changed

+358
-41
lines changed

16 files changed

+358
-41
lines changed

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,20 @@ The format follows Keep a Changelog and the version numbers follow Semantic Vers
66

77
## [Unreleased]
88

9+
## [0.3.4] - 2026-03-26
10+
11+
### Changed
12+
13+
- Improved architecture labels so backend and other non-frontend flows explain what `simple`, `modular`, and `monorepo` mean more clearly during prompting.
14+
- Added a real-time file generation progress bar during scaffold creation so the CLI shows visible progress after printing the target directory.
15+
16+
### Fixed
17+
18+
- Prevented incompatible backend selections by filtering out `Drizzle` when `MongoDB` is chosen and by normalizing any `Drizzle + MongoDB` plan back to a supported setup.
19+
- Stopped generating `.nvmrc` for `LTS` and `Latest` Node strategies, avoiding broken version-manager lookups like `lts-latest` on systems that cannot resolve that alias.
20+
- Hardened dependency installation for `yarn` and `pnpm` by using Corepack when available and by keeping package-manager caches local to the generated project.
21+
- Removed stale lockfiles from other package managers before installation so the selected package manager stays authoritative after retries or partial runs.
22+
923
## [0.3.3] - 2026-03-26
1024

1125
### Added

docs/changelog.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,20 @@ Track what changed in DevForge CLI across releases, including scaffolding behavi
99
- [GitHub Releases](https://github.com/Ali-dev11/devforge/releases)
1010
- [Repository Changelog](https://github.com/Ali-dev11/devforge/blob/main/CHANGELOG.md)
1111

12+
## [0.3.4] - 2026-03-26
13+
14+
### Changed
15+
16+
- Improved architecture labels so backend and other non-frontend flows explain what `simple`, `modular`, and `monorepo` mean more clearly during prompting.
17+
- Added a real-time file generation progress bar during scaffold creation so the CLI shows visible progress after printing the target directory.
18+
19+
### Fixed
20+
21+
- Prevented incompatible backend selections by filtering out `Drizzle` when `MongoDB` is chosen and by normalizing any `Drizzle + MongoDB` plan back to a supported setup.
22+
- Stopped generating `.nvmrc` for `LTS` and `Latest` Node strategies, avoiding broken version-manager lookups like `lts-latest` on systems that cannot resolve that alias.
23+
- Hardened dependency installation for `yarn` and `pnpm` by using Corepack when available and by keeping package-manager caches local to the generated project.
24+
- Removed stale lockfiles from other package managers before installation so the selected package manager stays authoritative after retries or partial runs.
25+
1226
## [0.3.3] - 2026-03-26
1327

1428
### Added

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@ali-dev11/devforge",
3-
"version": "0.3.3",
3+
"version": "0.3.4",
44
"description": "Production-focused AI-native project scaffolding CLI for JavaScript and TypeScript teams.",
55
"license": "MIT",
66
"author": "Ali-dev11",

src/commands/init.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,14 @@ import {
1313
removeFile,
1414
saveResumeState,
1515
} from "../utils/fs.js";
16-
import { banner, info, step, success, warn } from "../utils/logger.js";
16+
import {
17+
banner,
18+
createProgressReporter,
19+
info,
20+
step,
21+
success,
22+
warn,
23+
} from "../utils/logger.js";
1724

1825
async function loadResumePlan(): Promise<ResumeState | undefined> {
1926
const resumePath = join(process.cwd(), RESUME_STATE_PATH);
@@ -64,7 +71,12 @@ export async function runInitCommand(options: CliOptions): Promise<void> {
6471
}
6572

6673
step(`Generating project in ${plan.targetDir}`);
67-
const generated = await generateProject(plan, environment);
74+
const progressReporter = createProgressReporter("Writing files");
75+
const generated = await generateProject(plan, environment, {
76+
onWrite: ({ current, total }) => {
77+
progressReporter({ current, total });
78+
},
79+
});
6880

6981
const installResult = runInstallers(plan, environment, options.skipInstall);
7082

src/constants.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -85,10 +85,10 @@ export const ARCHITECTURE_CHOICES: Array<{
8585
title: string;
8686
value: ArchitectureMode;
8787
}> = [
88-
{ title: "Simple", value: "simple" },
89-
{ title: "Modular", value: "modular" },
90-
{ title: "Monorepo", value: "monorepo" },
91-
{ title: "Microfrontend", value: "microfrontend" },
88+
{ title: "Simple (single app)", value: "simple" },
89+
{ title: "Modular (feature modules)", value: "modular" },
90+
{ title: "Monorepo (apps + packages)", value: "monorepo" },
91+
{ title: "Microfrontend (frontend only)", value: "microfrontend" },
9292
];
9393

9494
export const FRONTEND_FRAMEWORK_CHOICES: Array<{
@@ -217,7 +217,7 @@ export const ORM_CHOICES: Array<{
217217
}> = [
218218
{ title: "None", value: "none" },
219219
{ title: "Prisma", value: "prisma" },
220-
{ title: "Drizzle", value: "drizzle" },
220+
{ title: "Drizzle (SQL)", value: "drizzle" },
221221
];
222222

223223
export const DATABASE_CHOICES: Array<{

src/engines/decision.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,11 @@ export function normalizeProjectPlan(
166166
warnings.push("ORM selected without a database; defaulting to PostgreSQL.");
167167
plan.backend.database = "postgresql";
168168
}
169+
170+
if (plan.backend.orm === "drizzle" && plan.backend.database === "mongodb") {
171+
warnings.push("Drizzle ORM does not support MongoDB in generated backends; switching ORM to none and keeping MongoDB.");
172+
plan.backend.orm = "none";
173+
}
169174
}
170175

171176
if (plan.ai.tools.length === 0) {

src/engines/generator.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,14 @@ import {
1515
writeJson,
1616
} from "../utils/fs.js";
1717

18+
type GenerateProjectOptions = {
19+
onWrite?: (info: { current: number; total: number; path: string }) => void;
20+
};
21+
1822
export async function generateProject(
1923
plan: ProjectPlan,
2024
environment: EnvironmentInfo,
25+
options?: GenerateProjectOptions,
2126
): Promise<GeneratedProjectResult> {
2227
const projectExists = await pathExists(plan.targetDir);
2328

@@ -36,9 +41,19 @@ export async function generateProject(
3641
await ensureDir(plan.targetDir);
3742

3843
const files = [...buildProjectFiles(plan, environment), ...buildAiRuleFiles(plan)];
39-
const filesWritten = await writeGeneratedFiles(plan.targetDir, files);
44+
const totalWrites = files.length + 1;
45+
const filesWritten = await writeGeneratedFiles(plan.targetDir, files, options?.onWrite
46+
? ({ current, path }) => {
47+
options.onWrite?.({ current, total: totalWrites, path });
48+
}
49+
: undefined);
4050

4151
await writeJson(join(plan.targetDir, ".devforge", "project-plan.json"), plan);
52+
options?.onWrite?.({
53+
current: totalWrites,
54+
total: totalWrites,
55+
path: join(plan.targetDir, ".devforge", "project-plan.json"),
56+
});
4257

4358
return {
4459
targetDir: plan.targetDir,

src/engines/installer.ts

Lines changed: 121 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,34 @@
11
import { spawnSync } from "node:child_process";
2+
import { existsSync, mkdirSync, rmSync } from "node:fs";
3+
import { join } from "node:path";
24
import type { EnvironmentInfo, InstallResult, ProjectPlan } from "../types.js";
35

4-
function runCommand(command: string, args: string[], cwd: string): boolean {
6+
type CommandResult = {
7+
ok: boolean;
8+
output?: string;
9+
};
10+
11+
function runCommand(
12+
command: string,
13+
args: string[],
14+
cwd: string,
15+
env?: Record<string, string>,
16+
): CommandResult {
517
const result = spawnSync(command, args, {
618
cwd,
7-
stdio: "ignore",
819
encoding: "utf8",
20+
env: {
21+
...process.env,
22+
...env,
23+
},
24+
stdio: ["ignore", "pipe", "pipe"],
925
});
1026

11-
return result.status === 0;
27+
const output = `${result.stdout ?? ""}${result.stderr ?? ""}`.trim() || undefined;
28+
return {
29+
ok: result.status === 0,
30+
output,
31+
};
1232
}
1333

1434
function installArgs(packageManager: ProjectPlan["packageManager"]): string[] {
@@ -25,6 +45,82 @@ function installArgs(packageManager: ProjectPlan["packageManager"]): string[] {
2545
}
2646
}
2747

48+
function hasCorepack(cwd: string): boolean {
49+
return runCommand("corepack", ["--version"], cwd).ok;
50+
}
51+
52+
function installCacheEnv(
53+
packageManager: ProjectPlan["packageManager"],
54+
cwd: string,
55+
): Record<string, string> {
56+
const devforgeDir = join(cwd, ".devforge");
57+
mkdirSync(devforgeDir, { recursive: true });
58+
59+
const env: Record<string, string> = {};
60+
if (packageManager === "npm") {
61+
env.npm_config_cache = join(devforgeDir, "npm-cache");
62+
}
63+
64+
if (packageManager === "pnpm") {
65+
env.COREPACK_HOME = join(devforgeDir, "corepack");
66+
env.PNPM_STORE_DIR = join(devforgeDir, "pnpm-store");
67+
}
68+
69+
if (packageManager === "yarn") {
70+
env.COREPACK_HOME = join(devforgeDir, "corepack");
71+
env.YARN_CACHE_FOLDER = join(devforgeDir, "yarn-cache");
72+
}
73+
74+
return env;
75+
}
76+
77+
function installInvocation(
78+
packageManager: ProjectPlan["packageManager"],
79+
environment: EnvironmentInfo,
80+
cwd: string,
81+
): { command: string; args: string[]; label: string; available: boolean; env?: Record<string, string> } {
82+
const env = installCacheEnv(packageManager, cwd);
83+
84+
if ((packageManager === "pnpm" || packageManager === "yarn") && hasCorepack(cwd)) {
85+
return {
86+
command: "corepack",
87+
args: [packageManager, ...installArgs(packageManager)],
88+
label: `corepack ${packageManager} install`,
89+
available: true,
90+
env,
91+
};
92+
}
93+
94+
return {
95+
command: packageManager,
96+
args: installArgs(packageManager),
97+
label: `${packageManager} install`,
98+
available: environment.packageManagers[packageManager].installed,
99+
env,
100+
};
101+
}
102+
103+
function removeIncompatibleLockfiles(cwd: string, packageManager: ProjectPlan["packageManager"]): void {
104+
const lockfilesByManager: Record<ProjectPlan["packageManager"], string[]> = {
105+
npm: ["package-lock.json"],
106+
pnpm: ["pnpm-lock.yaml"],
107+
yarn: ["yarn.lock"],
108+
bun: ["bun.lock", "bun.lockb"],
109+
};
110+
const keep = new Set(lockfilesByManager[packageManager]);
111+
112+
for (const filename of Object.values(lockfilesByManager).flat()) {
113+
if (keep.has(filename)) {
114+
continue;
115+
}
116+
117+
const filePath = join(cwd, filename);
118+
if (existsSync(filePath)) {
119+
rmSync(filePath, { force: true });
120+
}
121+
}
122+
}
123+
28124
export function runInstallers(
29125
plan: ProjectPlan,
30126
environment: EnvironmentInfo,
@@ -34,30 +130,44 @@ export function runInstallers(
34130
const skipped: string[] = [];
35131

36132
if (!skipInstall) {
37-
const managerStatus = environment.packageManagers[plan.packageManager];
133+
const invocation = installInvocation(plan.packageManager, environment, plan.targetDir);
38134

39-
if (!managerStatus.installed) {
135+
if (!invocation.available) {
40136
skipped.push(
41-
`${plan.packageManager} is not installed; generated project without installing dependencies.`,
137+
`${plan.packageManager} is not installed and Corepack is unavailable; generated project without installing dependencies.`,
42138
);
43-
} else if (runCommand(plan.packageManager, installArgs(plan.packageManager), plan.targetDir)) {
44-
executed.push(`${plan.packageManager} install`);
45139
} else {
46-
skipped.push(`${plan.packageManager} install failed; dependencies were not installed.`);
140+
removeIncompatibleLockfiles(plan.targetDir, plan.packageManager);
141+
const installResult = runCommand(
142+
invocation.command,
143+
invocation.args,
144+
plan.targetDir,
145+
invocation.env,
146+
);
147+
148+
if (installResult.ok) {
149+
executed.push(invocation.label);
150+
} else {
151+
skipped.push(
152+
installResult.output
153+
? `${invocation.label} failed: ${installResult.output.split(/\r?\n/)[0]}`
154+
: `${invocation.label} failed; dependencies were not installed.`,
155+
);
156+
}
47157
}
48158
} else {
49159
skipped.push("Dependency installation skipped by flag.");
50160
}
51161

52162
if (plan.git.initialize) {
53-
if (runCommand("git", ["init"], plan.targetDir)) {
163+
if (runCommand("git", ["init"], plan.targetDir).ok) {
54164
executed.push("git init");
55165
} else {
56166
skipped.push("git init failed.");
57167
}
58168

59169
if (plan.git.addRemote && plan.git.remoteUrl) {
60-
if (runCommand("git", ["remote", "add", "origin", plan.git.remoteUrl], plan.targetDir)) {
170+
if (runCommand("git", ["remote", "add", "origin", plan.git.remoteUrl], plan.targetDir).ok) {
61171
executed.push("git remote add origin");
62172
} else {
63173
skipped.push("git remote add origin failed.");

0 commit comments

Comments
 (0)