Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,33 @@ All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

## [1.1.0] - 2026-06-02

### Added
- **Python parity — 5 new Python addons** bringing Python to feature parity with Node.js:
- `--email` — SMTP email service via built-in `smtplib`; `app/core/email.py` + `app/services/external/email_service.py` with `send`, `send_welcome`, `send_password_reset`, `send_notification`
- `--s3` — AWS S3 / Cloudflare R2 / MinIO storage via `boto3`; `app/core/storage.py` + `app/services/external/storage_service.py` with `upload`, `get_presigned_url`, `delete`, `exists`; injects `boto3>=1.35.0` into `pyproject.toml`
- `--oauth` — Google + GitHub OAuth2 via `httpx` (already in base deps); `app/core/oauth.py`, `app/services/oauth_service.py`, `app/routes/api/v1/oauth.py`; routes at `/api/v1/oauth/google` and `/api/v1/oauth/github`
- `--api-docs` — Custom OpenAPI schema with rich description + tag metadata; `app/core/openapi.py` + `main.py` override; Swagger UI at `/docs`, ReDoc at `/redoc`
- `--websocket` — Native FastAPI WebSocket with JWT auth; `app/routes/ws/notifications.py` + `app/services/notification_service.py` (`NotificationManager` with `send_to_user`, `broadcast`)
- **Queue addons** — background job processing for both stacks:
- **Node.js `--queue`** — BullMQ + Redis: `src/plugins/queue.plugin.ts` (queue registry), `src/services/queue.service.ts` (wrapper with retry logic), `src/workers/example.worker.ts`; injects `bullmq>=5.0.0`
- **Python `--queue`** — arq + Redis (Redis already in base stack): `app/services/queue_service.py` (pool + `enqueue()` helper), `app/workers/example_worker.py` (`WorkerSettings` for arq CLI); injects `arq>=0.26.0`
- **`archgen config` command** — preset management via `.archgenrc.json`:
- `archgen config init` — interactive wizard to create `.archgenrc.json` with default language, author, docker/testing/ci preferences
- `archgen config show` — display active preset (searches upward from cwd)
- `archgen config reset` — delete `.archgenrc.json` from cwd
- `archgen create` now auto-loads preset; CLI flags always take priority
- **Interactive addon discovery** — `archgen create` interactive mode now shows a multiselect for all extra addons (websocket, oauth, api-docs, email, s3, queue) so users can discover and enable them without memorising flags
- **`--queue` flag** — new global flag for both Node and Python

### Fixed
- CLI flag descriptions updated: removed stale "Node.js only" labels from `--websocket`, `--oauth`, `--api-docs`, `--email`, `--s3` which now also work for Python

### Tests
- **17 new unit tests** in `python-plugin.test.ts` covering all 5 new Python addons (metadata + apply/skip logic)
- Total: **142 unit tests** (up from 125)

## [1.0.8] - 2026-05-14

### Added
Expand Down
108 changes: 108 additions & 0 deletions cli/command/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import path from "path";
import { Command } from "commander";
import prompts from "prompts";
import chalk from "chalk";
import { writePreset, loadPreset, findPresetFile, ArchGenPreset } from "../../core/config-preset";
import { logger } from "../../core/logger";

export const configCommand = new Command("config")
.description("Manage archgen project presets (.archgenrc.json)")
.addCommand(
new Command("init")
.description("Create a .archgenrc.json preset in the current directory")
.action(async () => {
const existing = findPresetFile(process.cwd());
if (existing) {
logger.warn(`Preset already exists at ${existing}`);
logger.info("Edit it directly or run 'archgen config show' to view it.");
return;
}

const answers = await prompts([
{
type: "select",
name: "language",
message: "Default language:",
choices: [
{ title: "Node.js (TypeScript + Fastify)", value: "node" },
{ title: "Python (FastAPI)", value: "python" },
],
},
{
type: "text",
name: "author",
message: "Default author name:",
initial: "",
},
{
type: "confirm",
name: "docker",
message: "Always include Docker?",
initial: false,
},
{
type: "confirm",
name: "testing",
message: "Always include testing setup?",
initial: false,
},
{
type: "confirm",
name: "ci",
message: "Always include GitHub Actions CI?",
initial: false,
},
], {
onCancel: () => {
logger.info("\nCancelled.");
process.exit(0);
},
});

const preset: ArchGenPreset = {
language: answers.language,
...(answers.author ? { author: answers.author } : {}),
docker: answers.docker,
testing: answers.testing,
ci: answers.ci,
};

const filePath = await writePreset(process.cwd(), preset);
console.log();
console.log(chalk.green(` ✔ Created ${filePath}`));
console.log(chalk.dim(" Edit it freely — all keys are optional."));
console.log();
}),
)
.addCommand(
new Command("show")
.description("Show the active preset (searches upward from current directory)")
.action(() => {
const filePath = findPresetFile(process.cwd());
if (!filePath) {
logger.info("No .archgenrc.json found. Run 'archgen config init' to create one.");
return;
}
const preset = loadPreset(filePath);
console.log();
console.log(chalk.bold(` Active preset: ${path.relative(process.cwd(), filePath)}\n`));
for (const [k, v] of Object.entries(preset)) {
console.log(` ${chalk.cyan(k.padEnd(14))} ${chalk.white(String(v))}`);
}
console.log();
}),
)
.addCommand(
new Command("reset")
.description("Delete .archgenrc.json in the current directory")
.action(async () => {
const filePath = path.join(process.cwd(), ".archgenrc.json");
const { default: fs } = await import("fs-extra");
if (!fs.existsSync(filePath)) {
logger.info("No .archgenrc.json in the current directory.");
return;
}
await fs.remove(filePath);
logger.success("Removed .archgenrc.json");
}),
);
22 changes: 16 additions & 6 deletions cli/command/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ArchGen } from "../../core/archgen";
import { ArchGenError } from "../../core/errors";
import { logger } from "../../core/logger";
import { promptMissingOptions } from "../prompts";
import { findPresetFile, loadPreset, mergePreset } from "../../core/config-preset";

const VALID_NODE_DATABASES = ["mysql", "postgresql"];
const VALID_PYTHON_DATABASES = ["postgresql", "sqlite"];
Expand All @@ -13,14 +14,15 @@ export const createCommand = new Command("create")
.option("--docker", "Include Docker setup", false)
.option("--testing", "Include testing setup", false)
.option("--ci", "Include GitHub Actions CI workflow", false)
.option("--husky", "Include Husky + lint-staged setup (Node.js only)")
.option("--websocket", "Include WebSocket support via Socket.io (Node.js only)")
.option("--oauth", "Include OAuth2 providers — Google + GitHub (Node.js only)")
.option("--api-docs", "Include Scalar API reference UI (Node.js only)")
.option("--husky", "Include Husky + lint-staged setup (Node only)")
.option("--websocket", "Include WebSocket support (Socket.io for Node, native for Python)")
.option("--oauth", "Include OAuth2 providers — Google + GitHub")
.option("--api-docs", "Include API reference UI (Scalar for Node, built-in OpenAPI for Python)")
.option("--claude-code", "Include Claude Code setup (CLAUDE.md + .claude/skills/)")
.option("--cursor", "Include Cursor agent setup (.cursor/skills/)")
.option("--email", "Include Resend email service (Node.js only)")
.option("--s3", "Include AWS S3 / Cloudflare R2 storage service (Node.js only)")
.option("--email", "Include email service (SMTP)")
.option("--s3", "Include AWS S3 / Cloudflare R2 / MinIO storage service")
.option("--queue", "Include background job queue (BullMQ for Node, arq for Python)")
.option("--all", "Include all addons (docker + testing + ci)", false)
.option("-a, --author <n>", "Author name")
.option("-d, --description <desc>", "Project description")
Expand All @@ -37,6 +39,14 @@ export const createCommand = new Command("create")
options.ci = true;
}

// Merge .archgenrc.json preset (CLI flags take priority)
const presetFile = findPresetFile();
if (presetFile) {
const preset = loadPreset(presetFile);
options = mergePreset(preset, options as Record<string, unknown>) as typeof options;
logger.debug(`Loaded preset from ${presetFile}`);
}

// Database validation is deferred until after prompts resolve the language
const finalOptions = await promptMissingOptions(projectName, options);

Expand Down
2 changes: 2 additions & 0 deletions cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { addCommand } from "./command/add";
import { upgradeCommand } from "./command/upgrade";
import { doctorCommand } from "./command/doctor";
import { completionCommand } from "./command/completion";
import { configCommand } from "./command/config";
import { logger } from "../core/logger";
import { setSpinnerLevel } from "../core/spinner";
import { checkForUpdate } from "../core/update-notifier";
Expand Down Expand Up @@ -93,6 +94,7 @@ program.addCommand(infoCommand);
program.addCommand(addCommand);
program.addCommand(upgradeCommand);
program.addCommand(doctorCommand);
program.addCommand(configCommand);
program.addCommand(completionCommand);

program.parse();
40 changes: 40 additions & 0 deletions cli/prompts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,35 @@ export async function promptMissingOptions(
});
}

const extraAddons = ["websocket", "oauth", "apiDocs", "email", "s3", "queue"] as const;
const anyExtraSet = extraAddons.some((k) => options[k] !== undefined);
if (!anyExtraSet) {
const fixedLang = options.language;
questions.push({
type: (_prev, values) => {
const lang = fixedLang ?? (values as GenerateOptions).language;
return lang === "node" || lang === "python" ? "multiselect" : null;
},
name: "extraAddons",
message: "Include extra addons? (Space to select, Enter to confirm)",
choices: (_prev, values) => {
const lang = fixedLang ?? (values as GenerateOptions).language;
const nodeOnly = lang === "node";
const all = [
{ title: "WebSocket", value: "websocket", selected: false },
{ title: "OAuth2 (Google + GitHub)", value: "oauth", selected: false },
{ title: "API Docs", value: "apiDocs", selected: false },
{ title: "Email (SMTP)", value: "email", selected: false },
{ title: "Storage (S3 / R2 / MinIO)", value: "s3", selected: false },
{ title: "Queue (BullMQ / arq)", value: "queue", selected: false },
{ title: "Husky + lint-staged", value: "husky", selected: false, disabled: !nodeOnly },
];
return nodeOnly ? all : all.filter((c) => c.value !== "husky");
},
hint: "none to skip",
});
}

if (questions.length === 0) return options;

const answers = await prompts(questions, {
Expand All @@ -136,5 +165,16 @@ export async function promptMissingOptions(
raw.cursor = (raw.aiAgents as string[]).includes("cursor");
delete raw.aiAgents;
}
if (Array.isArray(raw.extraAddons)) {
const selected = raw.extraAddons as string[];
raw.websocket = selected.includes("websocket");
raw.oauth = selected.includes("oauth");
raw.apiDocs = selected.includes("apiDocs");
raw.email = selected.includes("email");
raw.s3 = selected.includes("s3");
raw.queue = selected.includes("queue");
if (selected.includes("husky")) raw.husky = true;
delete raw.extraAddons;
}
return raw as unknown as GenerateOptions;
}
61 changes: 61 additions & 0 deletions core/config-preset.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import fs from "fs-extra";
import path from "path";

export interface ArchGenPreset {
language?: string;
author?: string;
description?: string;
database?: string;
docker?: boolean;
testing?: boolean;
ci?: boolean;
husky?: boolean;
websocket?: boolean;
oauth?: boolean;
apiDocs?: boolean;
email?: boolean;
s3?: boolean;
queue?: boolean;
claudeCode?: boolean;
cursor?: boolean;
}

const CONFIG_FILE = ".archgenrc.json";

export function findPresetFile(startDir: string = process.cwd()): string | null {
let dir = startDir;
for (let i = 0; i < 5; i++) {
const candidate = path.join(dir, CONFIG_FILE);
if (fs.existsSync(candidate)) return candidate;
const parent = path.dirname(dir);
if (parent === dir) break;
dir = parent;
}
return null;
}

export function loadPreset(filePath: string): ArchGenPreset {
try {
return fs.readJsonSync(filePath) as ArchGenPreset;
} catch {
return {};
}
}

export function mergePreset(preset: ArchGenPreset, cliOptions: Record<string, unknown>): Record<string, unknown> {
const merged: Record<string, unknown> = { ...preset };
for (const [k, v] of Object.entries(cliOptions)) {
if (v !== undefined && v !== false) {
merged[k] = v;
} else if (v === false && merged[k] === undefined) {
merged[k] = false;
}
}
return merged;
}

export async function writePreset(dir: string, preset: ArchGenPreset): Promise<string> {
const filePath = path.join(dir, CONFIG_FILE);
await fs.writeJson(filePath, preset, { spaces: 2 });
return filePath;
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@kidkender/archgen",
"version": "1.0.8",
"version": "1.1.0",
"description": "Generate production-ready Node.js and Python project structures in seconds",
"main": "dist/index.js",
"module": "dist/index.mjs",
Expand Down
2 changes: 1 addition & 1 deletion plugins/node/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ import { PluginConfig } from "../../types";
export const nodeConfig: PluginConfig = {
name: "node-typescript",
description: "Node.js Typescript backend with Fastify",
addons: ["docker", "testing", "ci", "husky", "websocket", "oauth", "api-docs", "claude-code", "cursor", "email", "s3"],
addons: ["docker", "testing", "ci", "husky", "websocket", "oauth", "api-docs", "claude-code", "cursor", "email", "s3", "queue"],
};
13 changes: 13 additions & 0 deletions plugins/node/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,11 @@ export class NodePlugin extends BasePlugin {
path: path.join(addonsPath, "s3"),
label: "Storage (AWS S3)",
},
{
condition: !!options.queue,
path: path.join(addonsPath, "queue"),
label: "Queue (BullMQ)",
},
];
}

Expand All @@ -117,6 +122,7 @@ export class NodePlugin extends BasePlugin {
extraDeps["@aws-sdk/client-s3"] = "^3.600.0";
extraDeps["@aws-sdk/s3-request-presigner"] = "^3.600.0";
}
if (addon === "queue") extraDeps["bullmq"] = "^5.0.0";

if (Object.keys(extraDeps).length === 0) return;

Expand Down Expand Up @@ -147,6 +153,7 @@ export class NodePlugin extends BasePlugin {
extraDeps["@aws-sdk/client-s3"] = "^3.600.0";
extraDeps["@aws-sdk/s3-request-presigner"] = "^3.600.0";
}
if (options.queue) extraDeps["bullmq"] = "^5.0.0";

if (Object.keys(extraDeps).length === 0) return;

Expand Down Expand Up @@ -231,6 +238,12 @@ export class NodePlugin extends BasePlugin {
console.log(" For Cloudflare R2 / MinIO, set S3_ENDPOINT too");
console.log(" Register storagePlugin in src/app.ts, then inject StorageService into your routes");
}
if (options.queue) {
console.log("");
console.log(" Queue (BullMQ) — register queuePlugin in src/app.ts");
console.log(" Use fastify.queues.getQueue('example') to enqueue jobs");
console.log(" Run workers: node dist/src/workers/example.worker.js");
}
console.log("");
}
}
Loading
Loading