alepha
Version:
Easy-to-use modern TypeScript framework for building many kind of applications.
1,577 lines (1,537 loc) • 175 kB
JavaScript
import { createRequire } from "node:module";
import { $atom, $hook, $inject, $module, $state, Alepha, AlephaError, __alephaRef, z } from "alepha";
import { $command, CliProvider, EnvUtils } from "alepha/command";
import { $logger, ConsoleColorProvider } from "alepha/logger";
import { FileSystemProvider, ShellProvider } from "alepha/system";
import { createHash } from "node:crypto";
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { readFile, readdir, stat } from "node:fs/promises";
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
import pkg from "alepha/package.json" with { type: "json" };
import { analyzer } from "vite-bundle-analyzer";
import { KV_DEFAULT_BINDING } from "alepha/cache";
import { SEND_EMAIL_DEFAULT_BINDING } from "alepha/email/cloudflare";
import { QUEUE_DEFAULT_BINDING, QUEUE_DEFAULT_MAX_RETRIES } from "alepha/queue";
import { promisify } from "node:util";
import { brotliCompress, gzip } from "node:zlib";
import { exec, spawn } from "node:child_process";
import { ServerSwaggerProvider } from "alepha/server/swagger";
import { pathToFileURL } from "node:url";
//#region ../../src/cli/core/atoms/appEntryOptions.ts
const appEntryOptions = $atom({
name: "alepha.cli.appEntry.options",
schema: z.object({
server: z.text().optional(),
browser: z.text().optional(),
style: z.text().optional()
}),
default: {}
});
//#endregion
//#region ../../src/cli/core/atoms/buildOptions.ts
/**
* Build options atom for CLI build command.
*
* Defines the available build configuration options with their defaults.
* Options can be overridden via alepha.config.ts or CLI flags.
*/
const buildOptions = $atom({
name: "alepha.cli.build.options",
description: "Build configuration options",
schema: z.object({
/**
* Generate build stats report.
*
* - `true` - Generate a static HTML report
* - `"json"` - Generate a JSON report
*/
stats: z.union([z.boolean(), z.enum(["json"])]).optional(),
/**
* Deployment target for the build output.
*
* - `docker` - Generate Dockerfile for containerized deployment
* - `vercel` - Generate Vercel deployment configuration (forces node runtime)
* - `cloudflare` - Generate Cloudflare Workers configuration (forces workerd runtime)
*/
target: z.enum([
"bare",
"docker",
"vercel",
"cloudflare",
"static"
]).optional(),
/**
* JavaScript runtime for the build output.
*
* - `node` - Node.js runtime (default)
* - `bun` - Bun runtime (uses bun export conditions)
* - `workerd` - Cloudflare Workers runtime (auto-set with cloudflare target)
*
* Note: Some targets force a specific runtime:
* - `cloudflare` always uses `workerd`
* - `vercel` always uses `node`
*/
runtime: z.enum([
"node",
"bun",
"workerd"
]).optional(),
/**
* Output directory configuration.
*/
output: z.object({
/**
* Root dist directory.
*
* @default "dist"
*/
dist: z.string().default("dist").optional(),
/**
* Public/client subdirectory.
*
* @default "public"
*/
public: z.string().default("public").optional()
}).optional(),
/**
* Vercel-specific deployment configuration.
*
* Note: Set `target: "vercel"` to enable Vercel deployment.
* This object is only for additional configuration.
*/
vercel: z.object({
projectName: z.string().optional(),
orgId: z.string().optional(),
projectId: z.string().optional(),
config: z.object({ crons: z.array(z.object({
path: z.string(),
schedule: z.string()
})).optional() }).optional()
}).optional(),
/**
* Cloudflare-specific deployment configuration.
*
* Note: Set `target: "cloudflare"` to enable Cloudflare deployment.
* This object is only for additional configuration.
*/
cloudflare: z.object({ config: z.json().optional() }).optional(),
/**
* Docker-specific deployment configuration.
*
* Note: Set `target: "docker"` to enable Docker deployment.
* This object is only for additional configuration.
*/
docker: z.object({
/**
* Base image for the Dockerfile (FROM instruction).
*
* @default "node:24-alpine" for node runtime
* @default "oven/bun:alpine" for bun runtime
*/
from: z.string().optional(),
/**
* Command to run in the Docker container.
*
* @default "node" for node runtime
* @default "bun" for bun runtime
*/
command: z.string().optional(),
/**
* Extra packages to install globally in the generated image.
*
* Each entry becomes a `RUN npm install --global --no-fund
* --no-audit <pkg> …` line inserted after `FROM` and before the
* app `COPY`. Use it for CLI tools the running app shells out to
* — typical example is `wrangler` for a service that deploys to
* Cloudflare on someone else's behalf.
*
* Ignored in `compile` mode (the distroless base has no `npm`).
*
* @example install: ["wrangler"]
*/
install: z.array(z.string()).optional(),
/**
* Docker build options (used when --image flag is passed).
*/
image: z.object({
/**
* Default image tag (name without version).
*
* Used when --image is provided without a full override:
* - `--image` → `tag:latest`
* - `--image=1.3.4` → `tag:1.3.4`
* - `--image=other/img:v1` → `other/img:v1` (full override)
*
* @example "myproject/myapp"
* @example "ghcr.io/myorg/myapp"
*/
tag: z.string(),
/**
* Additional arguments to pass to `docker build`.
*
* @example '--platform linux/amd64 --no-cache'
*/
args: z.string().optional(),
/**
* Auto-add OCI standard labels (revision, created, version).
*
* Adds:
* - org.opencontainers.image.revision (git commit SHA)
* - org.opencontainers.image.created (build timestamp)
* - org.opencontainers.image.version (from image tag)
*/
oci: z.boolean().optional()
}).optional(),
/**
* Compile the server entry to a single static binary using
* `bun build --compile`, then package it inside a minimal base image
* (distroless by default). Requires `runtime: "bun"`.
*
* When enabled:
* - the binary is produced at `<dist>/app` and the original `dist/server/`,
* `dist/index.js` and `dist/package.json` are removed
* - the generated Dockerfile uses a distroless base image and does not
* run `bun install` (everything is embedded in the binary)
* - any non-empty `dependencies` in the externals manifest causes the
* task to fail loudly (compile requires fully-bundled output)
*
* Pass `true` to enable with defaults, or an object to override.
*/
compile: z.union([z.boolean(), z.object({
/**
* Bun target triple, e.g. `bun-linux-x64-musl`,
* `bun-linux-arm64-musl`, or `bun-linux-x64-modern-musl`
* (AVX2 required).
*
* @default derived from host arch — always linux-musl.
*/
target: z.string().optional(),
/**
* Base image for the generated Dockerfile.
*
* @default "gcr.io/distroless/static-debian12"
*/
base: z.string().optional(),
/**
* Minify the compiled output.
*
* @default true
*/
minify: z.boolean().optional()
})]).optional()
}).optional(),
/**
* Static site deployment configuration.
*
* Note: Set `target: "static"` to enable static site generation.
*/
static: z.object({
/**
* Surge domain for deployment.
*
* If set, a CNAME file is written to dist/public/.
* If not set, a domain is auto-generated from package.json name.
*
* @example "my-app.surge.sh"
* @example "my-custom-domain.com"
*/
domain: z.string().optional() }).optional(),
/**
* PWA (Progressive Web App) configuration.
*
* Generates a web app manifest and enables installability.
* Requires a client-side bundle (React).
*/
pwa: z.object({
/**
* Full application name displayed on the splash screen
* and in the OS app switcher.
*/
name: z.string(),
/**
* Short name displayed on the home screen icon.
* Falls back to `name` if omitted.
*/
shortName: z.string().optional(),
/**
* Theme color used for the browser toolbar and OS chrome.
*
* @default "#ffffff"
*/
themeColor: z.string().optional(),
/**
* Background color for the splash screen.
*
* @default "#ffffff"
*/
backgroundColor: z.string().optional(),
/**
* Display mode for the installed PWA.
*
* - `standalone` - Looks like a native app (default)
* - `fullscreen` - Uses entire screen (games, immersive)
* - `minimal-ui` - Like standalone with minimal browser UI
* - `browser` - Standard browser tab
*
* @default "standalone"
*/
display: z.enum([
"standalone",
"fullscreen",
"minimal-ui",
"browser"
]).optional(),
/**
* Enable offline support via service worker.
*
* TODO: Not yet implemented.
*/
offline: z.boolean().optional()
}).optional()
}),
default: {}
});
//#endregion
//#region ../../src/cli/core/atoms/devOptions.ts
/**
* Dev options atom for CLI dev command.
*
* Defines the available dev configuration options with their defaults.
* Options can be overridden via alepha.config.ts or CLI flags.
*/
const devOptions = $atom({
name: "alepha.cli.dev.options",
description: "Dev configuration options",
schema: z.object({
/**
* Disable Vite React plugin.
*/
noViteReactPlugin: z.boolean().default(false).optional() }),
default: {}
});
//#endregion
//#region ../../src/cli/core/providers/AppEntryProvider.ts
/**
* Service for locating entry files in Alepha projects.
*
* Resolves application entry points for the CLI build pipeline.
*/
var AppEntryProvider = class {
fs = $inject(FileSystemProvider);
options = $state(appEntryOptions);
serverEntries = [
"main.server.ts",
"main.server.tsx",
"main.ts",
"main.tsx"
];
browserEntries = [
"main.browser.ts",
"main.browser.tsx",
"main.ts",
"main.tsx"
];
styleEntries = [
"main.css",
"styles.css",
"style.css"
];
/**
* Get application entry points.
*
* Server entry is required, an error is thrown if not found.
* Browser entry is optional.
*
* It will first check for custom entries in options, see appEntryOptions.
*/
async getAppEntry(root) {
const appEntry = {
root,
server: ""
};
if (this.options.server) {
const serverPath = this.fs.join(root, this.options.server);
if (!await this.fs.exists(serverPath)) throw new AlephaError(`Custom server entry not found: ${serverPath}`);
appEntry.server = this.options.server;
}
if (this.options.browser) {
const browserPath = this.fs.join(root, this.options.browser);
if (!await this.fs.exists(browserPath)) throw new AlephaError(`Custom browser entry not found: ${browserPath}`);
appEntry.browser = this.options.browser;
}
if (this.options.style) {
const stylePath = this.fs.join(root, this.options.style);
if (!await this.fs.exists(stylePath)) throw new AlephaError(`Custom style entry not found: ${stylePath}`);
appEntry.style = this.options.style;
}
const srcFiles = await this.fs.ls(this.fs.join(root, "src"));
if (!appEntry.server) {
for (const entry of this.serverEntries) if (srcFiles.includes(entry)) {
appEntry.server = this.fs.join("src", entry);
break;
}
}
if (!appEntry.server) {
const srcDir = this.fs.join(root, "src");
throw new AlephaError(`No server entry found. Tried: ${this.serverEntries.map((e) => this.fs.join(srcDir, e)).join(", ")}. Add a main.server.ts file or configure a custom entry in alepha.config.ts.`);
}
if (!appEntry.browser) {
for (const entry of this.browserEntries) if (srcFiles.includes(entry)) {
appEntry.browser = this.fs.join("src", entry);
break;
}
}
if (!appEntry.style) {
for (const entry of this.styleEntries) if (srcFiles.includes(entry)) {
appEntry.style = this.fs.join("src", entry);
break;
}
}
return appEntry;
}
};
//#endregion
//#region ../../src/cli/core/services/ViteUtils.ts
/**
* Vite integration utilities for the Alepha CLI.
*
* Centralizes all Vite-specific code: lazy loading, plugin creation,
* buffered logger, dev server management.
* When Vite is replaced, only this file needs to change.
*/
var ViteUtils = class {
fs = $inject(FileSystemProvider);
viteDevServer;
/**
* Lazy-load Vite (with rolldown-vite fallback).
*/
async importVite() {
try {
return createRequire(import.meta.url)("rolldown-vite");
} catch {
try {
return createRequire(import.meta.url)("vite");
} catch {
throw new AlephaError("Vite is not installed. Please install it with `npm install vite`.");
}
}
}
/**
* Lazy-load @vitejs/plugin-react (optional).
*/
async importViteReact() {
try {
const { default: viteReact } = createRequire(import.meta.url)("@vitejs/plugin-react");
return viteReact;
} catch {}
}
/**
* Create a Vite logger that buffers all messages instead of printing them.
* Useful for silent builds that only show output on failure.
*/
createBufferedLogger() {
const entries = [];
const loggedErrors = /* @__PURE__ */ new WeakSet();
const warnedMessages = /* @__PURE__ */ new Set();
let hasWarned = false;
return {
get hasWarned() {
return hasWarned;
},
info(msg) {
entries.push({
level: "info",
msg,
timestamp: /* @__PURE__ */ new Date()
});
},
warn(msg) {
hasWarned = true;
entries.push({
level: "warn",
msg,
timestamp: /* @__PURE__ */ new Date()
});
},
warnOnce(msg) {
if (warnedMessages.has(msg)) return;
warnedMessages.add(msg);
hasWarned = true;
entries.push({
level: "warn",
msg,
timestamp: /* @__PURE__ */ new Date()
});
},
error(msg, options) {
if (options?.error) loggedErrors.add(options.error);
entries.push({
level: "error",
msg,
timestamp: /* @__PURE__ */ new Date()
});
},
clearScreen() {},
hasErrorLogged(error) {
return loggedErrors.has(error);
},
flush() {
for (const entry of entries) {
const prefix = entry.level === "error" ? "\x1B[31m✖\x1B[0m" : entry.level === "warn" ? "\x1B[33m⚠\x1B[0m" : "\x1B[36mℹ\x1B[0m";
console.log(`${prefix} ${entry.msg}`);
}
},
getEntries() {
return [...entries];
},
clear() {
entries.length = 0;
warnedMessages.clear();
hasWarned = false;
}
};
}
/**
* Vite plugin that reads tsconfig.json `compilerOptions.paths` and converts
* them to Vite `resolve.alias` entries. Enables `@/*` → `src/*` style imports
* with zero config beyond tsconfig.json.
*/
createTsconfigPathsPlugin() {
return {
name: "alepha-tsconfig-paths",
async config(config) {
const root = config.root || process.cwd();
const tsconfigPath = join(root, "tsconfig.json");
let content;
try {
content = await readFile(tsconfigPath, "utf-8");
} catch {
return;
}
const clean = content.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
let tsconfig;
try {
tsconfig = JSON.parse(clean);
} catch {
return;
}
const paths = tsconfig?.compilerOptions?.paths;
if (!paths || typeof paths !== "object") return;
const alias = {};
for (const [pattern, targets] of Object.entries(paths)) {
if (!Array.isArray(targets) || targets.length === 0) continue;
const target = targets[0];
const aliasKey = pattern.replace(/\*$/, "");
const resolved = resolve(root, target.replace(/\*$/, "").replace(/^\.\//, ""));
alias[aliasKey] = aliasKey.endsWith("/") ? `${resolved}/` : resolved;
}
if (Object.keys(alias).length === 0) return;
return { resolve: { alias } };
}
};
}
/**
* Vite plugin that generates a preload manifest for SSR module preloading.
*
* Collects lazy import paths from $page definitions during transform,
* generates a manifest mapping short keys to resolved source paths,
* and injects only the short key into $page definitions.
*/
createSsrPreloadPlugin() {
let root = "";
const preloadMap = /* @__PURE__ */ new Map();
function generateKey(sourcePath) {
return createHash("md5").update(sourcePath).digest("hex").slice(0, 8);
}
return {
name: "alepha-preload",
configResolved(config) {
root = config.root;
},
transform(code, id) {
if (!id.match(/\.[tj]sx?$/)) return null;
if (id.includes("node_modules")) return null;
if (!code.includes("$page") || !code.includes("lazy")) return null;
const insertions = [];
const pageStartRegex = /\$page\s*\(\s*\{/g;
let pageMatch = pageStartRegex.exec(code);
while (pageMatch !== null) {
const objectStartIndex = pageMatch.index + pageMatch[0].length - 1;
let braceCount = 1;
let i = objectStartIndex + 1;
while (i < code.length && braceCount > 0) {
if (code[i] === "{") braceCount++;
else if (code[i] === "}") braceCount--;
i++;
}
if (braceCount !== 0) {
pageMatch = pageStartRegex.exec(code);
continue;
}
const objectEndIndex = i - 1;
const pageContent = code.slice(objectStartIndex, objectEndIndex + 1);
if (pageContent.includes("alepha.page.preload")) {
pageMatch = pageStartRegex.exec(code);
continue;
}
const lazyMatch = /lazy\s*:\s*\(\s*\)\s*=>\s*import\s*\(\s*['"]([^'"]+)['"]\s*\)/.exec(pageContent);
if (!lazyMatch) {
pageMatch = pageStartRegex.exec(code);
continue;
}
const importPath = lazyMatch[1];
const currentDir = dirname(id);
let resolvedPath;
if (importPath.startsWith(".")) resolvedPath = resolve(currentDir, importPath);
else if (importPath.startsWith("/")) resolvedPath = resolve(root, importPath.slice(1));
else {
pageMatch = pageStartRegex.exec(code);
continue;
}
let relativePath = relative(root, resolvedPath);
relativePath = relativePath.replace(/\\/g, "/");
if (!relativePath.match(/\.[tj]sx?$/)) relativePath = `${relativePath}.tsx`;
else if (relativePath.endsWith(".jsx")) relativePath = relativePath.replace(/\.jsx$/, ".tsx");
else if (relativePath.endsWith(".js")) relativePath = relativePath.replace(/\.js$/, ".ts");
const key = generateKey(relativePath);
preloadMap.set(key, relativePath);
const preloadProperty = `${!code.slice(0, objectEndIndex).trimEnd().endsWith(",") ? "," : ""} [Symbol.for("alepha.page.preload")]: "${key}"`;
insertions.push({
position: objectEndIndex,
text: preloadProperty
});
pageMatch = pageStartRegex.exec(code);
}
if (insertions.length === 0) return null;
let result = code;
for (let j = insertions.length - 1; j >= 0; j--) {
const { position, text } = insertions[j];
result = result.slice(0, position) + text + result.slice(position);
}
return {
code: result,
map: null
};
},
writeBundle(options) {
const outDir = options.dir || "";
if (outDir.includes("server")) return;
if (preloadMap.size > 0) {
const viteDir = join(outDir, ".vite");
if (!existsSync(viteDir)) mkdirSync(viteDir, { recursive: true });
const manifest = Object.fromEntries(preloadMap);
writeFileSync(join(viteDir, "preload-manifest.json"), JSON.stringify(manifest, null, 2));
}
}
};
}
generateIndexHtml(entry, opts) {
const style = entry.style;
const browser = entry.browser ?? entry.server;
return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>App</title>
<meta name="viewport" content="width=device-width, initial-scale=1"/>${opts?.pwa ? "\n<link rel=\"manifest\" href=\"/manifest.webmanifest\" />" : ""}
${style ? `<link rel="stylesheet" href="/${style}" />` : ""}
</head>
<body>
<div id="root"></div>
<script type="module" src="/${browser}"><\/script>
</body>
</html>
`.trim();
}
/**
* We need to close the Vite dev server after build is done.
*/
onReady = $hook({
on: "ready",
priority: "last",
handler: async () => {
await this.viteDevServer?.close();
}
});
onStop = $hook({
on: "stop",
handler: async () => {
await this.viteDevServer?.close();
}
});
async runAlepha(opts) {
const { createServer } = await this.importVite();
process.env.NODE_ENV = opts.mode;
process.env.ALEPHA_CLI_IMPORT = "true";
process.env.LOG_LEVEL ??= "warn";
process.env.APP_SECRET ??= "123456";
/**
* 01/26 Vite 7
* "runnerImport" doesn't work as expected here. (e.g. build docs fail)
* -> We still use devServer and ssrLoadModule for now.
* -> This is clearly a bad stuff, we need to find better way.
*/
this.viteDevServer = await createServer({
server: { middlewareMode: true },
appType: "custom",
logLevel: "silent",
plugins: [this.createTsconfigPathsPlugin()]
});
await this.viteDevServer.ssrLoadModule(opts.entry.server);
delete process.env.ALEPHA_CLI_IMPORT;
const alepha = globalThis.__alepha;
if (!alepha) throw new AlephaError("Alepha instance not found after loading entry module");
return alepha;
}
};
//#endregion
//#region ../../src/cli/core/providers/ViteBuildProvider.ts
var ViteBuildProvider = class {
alepha;
appEntry;
viteUtils = $inject(ViteUtils);
async init(opts) {
const alepha = await this.viteUtils.runAlepha({
entry: opts.entry,
mode: "production"
});
this.alepha = alepha;
this.appEntry = opts.entry;
return alepha;
}
hasClient() {
if (!this.alepha) throw new AlephaError("ViteBuildProvider not initialized");
try {
this.alepha.inject("ReactServerProvider");
return true;
} catch {
return false;
}
}
};
//#endregion
//#region ../../src/cli/core/services/AlephaCliUtils.ts
/**
* Core utility service for CLI commands.
*
* Provides:
* - Command execution
* - File editing helpers
* - Drizzle/ORM utilities
* - Environment loading
*/
var AlephaCliUtils = class {
log = $logger();
fs = $inject(FileSystemProvider);
envUtils = $inject(EnvUtils);
boot = $inject(AppEntryProvider);
shell = $inject(ShellProvider);
viteUtils = $inject(ViteUtils);
alepha = $inject(Alepha);
/**
* Execute a command with inherited stdio.
*/
async exec(command, options = {}) {
await this.shell.run(command, {
root: options.root,
env: options.env,
resolve: !options.global,
capture: options.capture
});
}
/**
* Resolve the absolute path to a toolchain binary that ships embedded in
* `alepha`'s own `dependencies` (typescript, vite, vitest, @biomejs/biome,
* drizzle-kit).
*
* The CLI runs the result via `node <path>` so the toolchain works under
* every package manager — including pnpm with a strict node-linker, where
* a transitive dependency's bin is NOT hoisted into the project's
* `node_modules/.bin`. Resolution starts from `alepha`'s own location, so
* the version is whatever `alepha` shipped — the project never pins it.
*
* @param pkg - npm package name (e.g. `"typescript"`)
* @param binName - which `bin` entry to use (e.g. `"tsc"`); defaults to the
* package's only/first bin
*/
resolveBin(pkg, binName) {
const require = createRequire(import.meta.url);
let pkgDir;
for (const nm of require.resolve.paths(pkg) ?? []) {
const candidate = join(nm, pkg);
if (existsSync(join(candidate, "package.json"))) {
pkgDir = candidate;
break;
}
}
if (!pkgDir) throw new AlephaError(`Cannot locate package '${pkg}' — is it installed alongside alepha?`);
const bin = JSON.parse(readFileSync(join(pkgDir, "package.json"), "utf8")).bin;
if (!bin) throw new AlephaError(`Package '${pkg}' declares no 'bin' entry`);
const rel = typeof bin === "string" ? bin : bin[binName ?? pkg] ?? Object.values(bin)[0];
if (!rel) throw new AlephaError(`Package '${pkg}' has no bin named '${binName}'`);
return join(pkgDir, rel);
}
/**
* Write a configuration file to node_modules/.alepha directory.
*/
async writeConfigFile(name, content, root = process.cwd()) {
const dir = this.fs.join(root, "node_modules", ".alepha");
await this.fs.mkdir(dir, { recursive: true }).catch(() => null);
const path = this.fs.join(dir, name);
await this.fs.writeFile(path, content);
this.log.debug(`Config file written: ${path}`);
return path;
}
async loadAlephaFromServerEntryFile(opts) {
let entry;
if ("root" in opts) entry = await this.boot.getAppEntry(opts.root);
else entry = opts.entry;
return await this.viteUtils.runAlepha({
entry,
mode: opts.mode
});
}
/**
* Load environment variables from a .env file.
*/
async loadEnv(root, files = [".env"]) {
await this.envUtils.loadEnv(root, files);
}
async exists(root, path) {
return this.fs.exists(this.fs.join(root, path));
}
/**
* Check if a command is installed and available in the system PATH.
*/
isInstalledAsync(cmd) {
return this.shell.isInstalled(cmd);
}
/**
* Get the current git revision (commit SHA).
*
* @returns The short commit SHA or "unknown" if not in a git repo
*/
async getGitRevision() {
try {
return (await this.shell.run("git rev-parse --short HEAD", { capture: true })).trim();
} catch {
return "unknown";
}
}
/**
* Get the user's email from git config.
*
* @returns The git user email or undefined if not configured
*/
async getGitEmail() {
try {
return (await this.shell.run("git config user.email", { capture: true })).trim() || void 0;
} catch {
return;
}
}
};
//#endregion
//#region ../../src/cli/core/alephaPackageJson.ts
const alephaPackageJson = pkg;
const version = pkg.version;
//#endregion
//#region ../../src/cli/core/services/PackageManagerUtils.ts
/**
* Utility service for package manager operations.
*
* Handles detection, installation, and cleanup for:
* - Yarn
* - npm
* - pnpm
* - Bun
*/
var PackageManagerUtils = class {
log = $logger();
fs = $inject(FileSystemProvider);
alepha = $inject(Alepha);
/**
* Detect the package manager used in the project.
* Checks current directory first, then workspace root if in a monorepo.
*/
async getPackageManager(root, pm) {
if (pm) return pm;
if (this.alepha.isBun()) return "bun";
if (await this.fs.exists(this.fs.join(root, "bun.lock"))) return "bun";
if (await this.fs.exists(this.fs.join(root, "yarn.lock"))) return "yarn";
if (await this.fs.exists(this.fs.join(root, "pnpm-lock.yaml"))) return "pnpm";
if (await this.fs.exists(this.fs.join(root, "package-lock.json"))) return "npm";
const workspace = await this.getWorkspaceContext(root);
if (workspace.packageManager) return workspace.packageManager;
return "npm";
}
/**
* Detect workspace context when inside a monorepo package.
*
* Checks if we're inside a workspace package by walking up to 3 levels
* for workspace indicators like lockfiles and config files.
* This covers both standard layouts (packages/my-pkg) and deeper nesting
* (packages/scope/my-pkg).
*
* @param root - The current package directory
* @returns Workspace context with root path, PM, and config presence
*/
async getWorkspaceContext(root) {
const noContext = {
isPackage: false,
workspaceRoot: null,
packageManager: null,
config: {
biomeJson: false,
editorconfig: false,
tsconfigJson: false
}
};
for (let depth = 2; depth <= 3; depth++) {
const segments = Array.from({ length: depth }, () => "..");
const candidate = this.fs.join(root, ...segments);
if (candidate === root) break;
const result = await this.checkWorkspaceRoot(candidate);
if (result) return result;
}
return noContext;
}
async checkWorkspaceRoot(candidate) {
const [hasYarnLock, hasPnpmLock, hasNpmLock, hasBunLock] = await Promise.all([
this.fs.exists(this.fs.join(candidate, "yarn.lock")),
this.fs.exists(this.fs.join(candidate, "pnpm-lock.yaml")),
this.fs.exists(this.fs.join(candidate, "package-lock.json")),
this.fs.exists(this.fs.join(candidate, "bun.lock"))
]);
if (!(hasYarnLock || hasPnpmLock || hasNpmLock || hasBunLock)) return null;
const [hasBiome, hasEditorConfig, hasTsConfig, hasPackageJson] = await Promise.all([
this.fs.exists(this.fs.join(candidate, "biome.json")),
this.fs.exists(this.fs.join(candidate, ".editorconfig")),
this.fs.exists(this.fs.join(candidate, "tsconfig.json")),
this.fs.exists(this.fs.join(candidate, "package.json"))
]);
if (!hasPackageJson) return null;
let packageManager = null;
if (hasYarnLock) packageManager = "yarn";
else if (hasPnpmLock) packageManager = "pnpm";
else if (hasBunLock) packageManager = "bun";
else if (hasNpmLock) packageManager = "npm";
return {
isPackage: true,
workspaceRoot: candidate,
packageManager,
config: {
biomeJson: hasBiome,
editorconfig: hasEditorConfig,
tsconfigJson: hasTsConfig
}
};
}
/**
* Get the install command for a package.
*/
async getInstallCommand(root, packageName, dev = true) {
const pm = await this.getPackageManager(root);
let cmd;
switch (pm) {
case "yarn":
cmd = `yarn add ${dev ? "-D" : ""} ${packageName}`;
break;
case "pnpm":
cmd = `pnpm add ${dev ? "-D" : ""} ${packageName}`;
break;
case "bun":
cmd = `bun add ${dev ? "-d" : ""} ${packageName}`;
break;
default: cmd = `npm install ${dev ? "--save-dev" : ""} ${packageName}`;
}
return cmd.replace(/\s+/g, " ").trim();
}
/**
* Check if a dependency is installed in the project.
*/
async hasDependency(root, packageName) {
try {
const pkg = await this.readPackageJson(root);
return !!(pkg.dependencies?.[packageName] || pkg.devDependencies?.[packageName]);
} catch {
return false;
}
}
/**
* Check if Expo is present in the project.
*/
async hasExpo(root) {
return this.hasDependency(root, "expo");
}
/**
* Check if React is present in the project.
*/
async hasReact(root) {
return this.hasDependency(root, "react");
}
/**
* Install a dependency if it's missing from the project.
* Optionally checks workspace root for the dependency in monorepo setups.
*/
async ensureDependency(root, packageName, options = {}) {
const { dev = true, checkWorkspace = false } = options;
if (await this.hasDependency(root, packageName)) {
this.log.debug(`Dependency '${packageName}' is already installed`);
return;
}
if (checkWorkspace) {
const workspace = await this.getWorkspaceContext(root);
if (workspace.workspaceRoot) {
if (await this.hasDependency(workspace.workspaceRoot, packageName)) {
this.log.debug(`Dependency '${packageName}' is already installed in workspace root`);
return;
}
}
}
const cmd = await this.getInstallCommand(root, packageName, dev);
if (options.run) await options.run(cmd, {
alias: `add ${packageName}`,
root
});
else if (options.exec) {
this.log.debug(`Installing ${packageName}`);
await options.exec(cmd, {
global: true,
root
});
}
}
async ensureYarn(root) {
const yarnrcPath = this.fs.join(root, ".yarnrc.yml");
if (!await this.fs.exists(yarnrcPath)) await this.fs.writeFile(yarnrcPath, "nodeLinker: node-modules");
await this.removeAllPmFilesExcept(root, "yarn");
}
async ensureBun(root) {
await this.removeAllPmFilesExcept(root, "bun");
}
async ensurePnpm(root) {
await this.removeAllPmFilesExcept(root, "pnpm");
}
async ensureNpm(root) {
await this.removeAllPmFilesExcept(root, "npm");
}
async removeAllPmFilesExcept(root, except) {
if (except !== "yarn") await this.removeYarn(root);
if (except !== "pnpm") await this.removePnpm(root);
if (except !== "npm") await this.removeNpm(root);
if (except !== "bun") await this.removeBun(root);
}
async removeYarn(root) {
await this.removeFiles(root, [
".yarn",
".yarnrc.yml",
"yarn.lock"
]);
await this.editPackageJson(root, (pkg) => {
pkg.packageManager = void 0;
return pkg;
});
}
async removePnpm(root) {
await this.removeFiles(root, ["pnpm-lock.yaml", "pnpm-workspace.yaml"]);
await this.editPackageJson(root, (pkg) => {
pkg.packageManager = void 0;
return pkg;
});
}
async removeNpm(root) {
await this.removeFiles(root, ["package-lock.json"]);
}
async removeBun(root) {
await this.removeFiles(root, ["bun.lockb", "bun.lock"]);
}
async readPackageJson(root) {
const content = await this.fs.createFile({ path: this.fs.join(root, "package.json") }).text();
return JSON.parse(content);
}
async writePackageJson(root, content) {
await this.fs.writeFile(this.fs.join(root, "package.json"), JSON.stringify(content, null, 2));
}
async editPackageJson(root, editFn) {
try {
const updated = editFn(await this.readPackageJson(root));
await this.writePackageJson(root, updated);
} catch {}
}
async ensurePackageJson(root, modes) {
const packageJsonPath = this.fs.join(root, "package.json");
if (!await this.fs.exists(packageJsonPath)) {
const content = {
name: basename(root) || "app",
private: true,
...this.generatePackageJsonContent(modes)
};
await this.writePackageJson(root, content);
return content;
}
const packageJson = await this.readPackageJson(root);
const newContent = this.generatePackageJsonContent(modes);
packageJson.type = "module";
packageJson.dependencies ??= {};
packageJson.devDependencies ??= {};
packageJson.scripts ??= {};
Object.assign(packageJson.dependencies, newContent.dependencies);
Object.assign(packageJson.devDependencies, newContent.devDependencies);
Object.assign(packageJson.scripts, newContent.scripts);
await this.writePackageJson(root, packageJson);
return packageJson;
}
generatePackageJsonContent(modes) {
const alephaDeps = alephaPackageJson.devDependencies;
const dependencies = { alepha: `^${version}` };
const devDependencies = {};
const scripts = {
dev: "alepha dev",
build: "alepha build",
test: "alepha test",
lint: "alepha lint",
typecheck: "alepha typecheck",
verify: "alepha verify"
};
if (modes.tailwind) {
devDependencies.tailwindcss = alephaDeps.tailwindcss;
devDependencies["@tailwindcss/vite"] = alephaDeps["@tailwindcss/vite"];
}
if (modes.react) {
dependencies.react = alephaDeps.react;
dependencies["react-dom"] = alephaDeps["react-dom"];
devDependencies["@types/react"] = alephaDeps["@types/react"];
}
return {
type: "module",
dependencies,
devDependencies,
scripts
};
}
async removeFiles(root, files) {
await Promise.all(files.map((file) => this.fs.rm(this.fs.join(root, file), {
force: true,
recursive: true
})));
}
};
//#endregion
//#region ../../src/cli/core/templates/agentMd.ts
const agentMd = () => {
return `# AGENTS.md
This is an **Alepha** project.
## Rules
- Always check \`node_modules/alepha/src/\` before suggesting npm packages
- Use \`t\` from Alepha for schemas (not Zod)
- Use \`protected\` instead of \`private\` for class members
- Import with file extensions: \`import { User } from "./User.ts"\`
## Commands
\`\`\`bash
alepha lint # Format and lint
alepha typecheck # Type checking
alepha test # Run tests
alepha build # Build
alepha platform plan # Show planned cloud topology (requires platform plugin)
alepha platform up # Provision + deploy to a configured environment
alepha platform status # Inspect deployed resources
\`\`\`
## Testing
- Specs live in \`test/\`, named \`*.spec.ts\`.
- Run with \`alepha test\` (Vitest, embedded in alepha — nothing to install).
- \`test/dummy.spec.ts\` is the starting example; \`Alepha.create()\` is the
entry point and \`.inject(...)\` resolves providers.
## Cloud deployment (Cloudflare Workers)
Add the \`platform\` plugin to \`alepha.config.ts\` to manage cloud
provisioning, deploy, secrets, and DB migrations end-to-end:
\`\`\`ts
import { defineConfig } from "alepha/cli/config";
import { platform } from "alepha/cli/platform";
export default defineConfig({
plugins: [
platform({
environments: {
production: {
adapter: "cloudflare",
domain: "yourapp.com",
// zone: "yourapp.com", // required only for wildcard domains
// jurisdiction: "eu", // optional: EU data residency
},
},
}),
],
});
\`\`\`
Then: \`alepha platform up --env production\` (auth via \`wrangler login\` on first run).
Supported adapters: \`cloudflare\`, \`vercel\`. The Cloudflare adapter provisions
D1 (or Hyperdrive when \`DATABASE_URL\` is postgres), KV, R2, Queues, and pushes
secrets via \`wrangler secret bulk\`. Set \`build.target: "cloudflare"\` in
\`alepha.config.ts\` if you only want the build artifact without the orchestrator.
## Documentation
- Framework source: \`node_modules/alepha/src/\`
- Docs: https://alepha.dev/llms.txt
`.trim();
};
//#endregion
//#region ../../src/cli/core/templates/alephaConfigTs.ts
/**
* Template for alepha.config.ts with documented options.
*/
const alephaConfigTs = () => {
return `import { defineConfig } from "alepha/cli/config";
// import { platform } from "alepha/cli/platform";
export default defineConfig({
//
// entry: {
// server: "src/main.server.ts",
// browser: "src/main.browser.ts",
// style: "src/main.css",
// },
//
// build: {
// target: "docker",
// runtime: "node",
// },
//
// env: {
// VITE_BUILD_DATE: new Date().toISOString(),
// VITE_VERSION: pkg.version,
// },
//
// // Deploy to Cloudflare in ~10s: \`alepha platform up --env production\`
// // Requires \`wrangler login\` once. D1, R2, KV, Queues and cron triggers
// // are auto-provisioned from your $repository / $bucket / $cache / $queue
// // / $scheduler primitives — no wrangler.toml to maintain.
// plugins: [
// platform({
// environments: {
// production: { adapter: "cloudflare", domain: "myapp.com" },
// preview: { adapter: "cloudflare" }, // workers.dev subdomain
// },
// }),
// ],
});
`;
};
//#endregion
//#region ../../src/cli/core/templates/apiHelloControllerTs.ts
const apiHelloControllerTs = (options = {}) => {
return `import { $action } from "alepha/server";
import { helloResponseSchema } from "../schemas/helloResponseSchema.ts";
export class HelloController {
hello = $action({
path: "/hello",
schema: {
response: helloResponseSchema,
},
handler: () => ({
appName: "${(options.appName || "my-app").split(/[-_]/).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ")}",
serverTime: new Date().toISOString(),
}),
});
}
`.trim();
};
//#endregion
//#region ../../src/cli/core/templates/apiHelloResponseSchemaTs.ts
const apiHelloResponseSchemaTs = () => {
return `import { type Static, z } from "alepha";
export const helloResponseSchema = z.object({
appName: z.text(),
serverTime: z.datetime(),
});
export type HelloResponse = Static<typeof helloResponseSchema>;
`.trim();
};
//#endregion
//#region ../../src/cli/core/templates/apiIndexTs.ts
const apiIndexTs = (options = {}) => {
const { appName = "app" } = options;
return `
import { $module } from "alepha";
import { HelloController } from "./controllers/HelloController.ts";
export const ApiModule = $module({
name: "${appName}.api",
services: [HelloController],
});
`.trim();
};
//#endregion
//#region ../../src/cli/core/templates/biomeJson.ts
const biomeJson = () => `
{
"$schema": "https://biomejs.dev/schemas/latest/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git"
},
"files": {
"ignoreUnknown": true,
"includes": ["**", "!node_modules", "!dist"]
},
"formatter": {
"enabled": true,
"useEditorconfig": true
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"a11y": {
"useFocusableInteractive": "off",
"useSemanticElements": "off",
"useKeyWithClickEvents": "off",
"useAriaPropsForRole": "off",
"noLabelWithoutControl": "off"
},
"correctness": {
"useExhaustiveDependencies": "off"
},
"suspicious": {
"noArrayIndexKey": "off",
"noExplicitAny": "off",
"noDocumentCookie": "off"
},
"style": {
"noNonNullAssertion": "off"
}
},
"domains": {
"react": "recommended"
}
},
"css": {
"parser": {
"cssModules": false,
"tailwindDirectives": true
}
},
"assist": {
"actions": {
"source": {
"organizeImports": "on"
}
}
}
}
`.trim();
//#endregion
//#region ../../src/cli/core/templates/dummySpecTs.ts
/**
* Starter spec written by `alepha init`. It doubles as a worked example:
* it shows the test location (`test/`), the `*.spec.ts` naming, and the
* canonical Alepha entry point — `Alepha.create()` + `.inject(...)`.
*
* Tests run with `alepha test` (Vitest, embedded in alepha). Replace this
* with real specs as the project grows.
*/
const dummySpecTs = () => `
import { Alepha } from "alepha";
import { expect, test } from "vitest";
test("alepha app can be created", () => {
const alepha = Alepha.create();
expect(alepha).toBeDefined();
expect(alepha.inject).toBeTypeOf("function");
});
`.trim();
//#endregion
//#region ../../src/cli/core/templates/editorconfig.ts
const editorconfig = () => `
# https://editorconfig.org
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = space
indent_size = 2
`.trim();
//#endregion
//#region ../../src/cli/core/templates/gitignore.ts
const gitignore = () => `
# Dependencies
node_modules/
# Build outputs
dist/
.vite/
# Environment files
.env
.env.*
!.env.example
# IDE
.idea/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
# Logs
*.log
logs/
# Test coverage
coverage/
# Yarn
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions
.pnp.*
`.trim();
//#endregion
//#region ../../src/cli/core/templates/logoSvg.ts
const logoSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="300" height="300">
<svg xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="b" x1="142.3" x2="159.6" y1="123.3" y2="176.7" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#33a72c"/>
<stop offset="1" stop-color="#2d8d40"/>
</linearGradient>
<linearGradient id="c" x1="61.6" x2="100.7" y1="218.5" y2="174" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#298e35"/>
<stop offset="1" stop-color="#327952"/>
</linearGradient>
<linearGradient id="d" x1="262.7" x2="242.2" y1="178.4" y2="220.1" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#32a62d"/>
<stop offset="1" stop-color="#2d8d40"/>
</linearGradient>
<linearGradient id="e" x1="81.2" x2="69.1" y1="126.7" y2="92.3" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#299a2c"/>
<stop offset="1" stop-color="#51be40"/>
</linearGradient>
<clipPath id="a">
<path fill="none" d="M0 0h300v241H0z"/>
</clipPath>
</defs>
<g fill="none" stroke-miterlimit="10" clip-path="url(#a)" font-family="none" font-size="none" font-weight="300" style="mix-blend-mode:normal" text-anchor="middle">
<path fill="url(#b)" d="M54 174a182 182 0 0 1 106-51c19-2 37-6 54-15l16-11 3-4 1 1 1 4 1 6v9c0 6-2 12-5 18l-1 4-7 8c-5 7-12 14-19 19l-4 2-11 7v1l-1-1-3 2a120 120 0 0 1-30 8l-8 1-20-1c-21-3-44-10-64 0-5 2-9 4-12 8l-9 9h1l14-9c7-4 15-6 23-7h7l25 3 3 1-2 5-3 5c-4 7-10 14-15 18l-4 3c-12 8-25 12-39 13h-5l-5 1h-8l-7-1H15c0-3 4-7 5-9l7-11 5-8c6-10 14-19 22-28z"/>
<path fill="#1d524a" d="M178 141c9-1 17-4 24-8l8-4-6 4c-9 7-20 11-32 13l-39 5h-5l-15 3 16 13 18 15-20-1c-21-3-44-10-64 0-5 2-9 4-12 8l-9 9h1l14-9c7-4 15-6 23-7h7l25 3 3 1-2 5-3 5c-4 7-10 14-15 18l-4 3c-12 8-25 12-39 13h-5l-5 1h-8l-7-1H15c0-3 4-7 5-9l7-11 5-8c6-10 14-19 22-28l4-2 23-14 11-5c21-7 43-9 65-9l21-3z"/>
<path fill="url(#c)" d="M57 189c7-4 15-6 23-7h7l25 3 3 1-2 5-3 5c-4 7-10 14-15 18l-4 3c-12 8-25 12-39 13h-5l-4-9-2-5 5-2 9-4 10-5 10-4c5-2 10-3 14-6l3-1h-1a1136 1136 0 0 0-43 14c1-6 6-14 9-19 1 0 0 0 0 0z"/>
<path fill="#2d6f4d" d="m41 216 5-2 9-4 5 3c9 4 18 6 27 4h4c-12 8-25 12-39 13h-5l-4-9-2-5zm16-27s1 0 0 0c-3 5-8 13-9 19l-15 8c0-3 1-9 3-11l3-3 4-4 14-9z"/>
<path fill="#49b63b" d="M42 198h1l-4 4 3-4z"/>
<path fill="#2d6f4d" d="m113 154 15-3 11 2 18 8c11 6 23 8 35 5l8-2-11 7v1l-1-1-3 2a120 120 0 0 1-30 8l-8 1-18-15-16-13z"/>
<path fill="#49b63b" d="M81 158c2-4 11-7 15-8 18-6 36-7 55-8l12-1h15l-21 3c-22 0-44 2-65 9l-11 5z"/>
<path fill="#2d913b" d="m110 52 2-5 9-14c7-8 19-14 29-18l10-3 12-6 1-1 1 1 1 5v6l6 12 5 10 12 26 2 5 4 10 7 17 4 8c-9 5-20 9-30 12l-16 3-7-14-6-14-5-10-1-3-4 7-13 27-6 11-6 2-24 9 2-4c11-16 14-36 11-56l-3 1-9 5 1-3 9-21 2-5z"/>
<path fill="#2d6f4d" d="m175 17 6 12 5 10 12 26 2 5 4 10 7 17 4 8c-9 5-20 9-30 12l-16 3-7-14-6-14-5-10-1-3c0-2 5-8 6-10l1-2c9-13 17-28 18-45v-5z"/>
<path fill="#1d524a" d="m175 17 6 12 5 10 12 26 2 5-3-1-13-8-7 4 3 7-4 1-2-3-5-12-5 7a136 136 0 0 1-10 15l-3 2-1-3c0-2 5-8 6-10l1-2c9-13 17-28 18-45v-5z"/>
<path fill="#2d6f4d" d="m186 39 12 26 2 5-3-1-13-8-7 4-6-12 8-7 7-7z"/>
<path fill="#2d913b" d="m177 65 7-4 13 8 3 1 4 10-3 7-7 14-2 8-1-4-6-19a99 99 0 0 0-8-21zm-3 5 2 3 12 32h-1l-8 5c-4 1-9 0-13-2l-1-1c-1-8 2-15 4-22 1-5 2-10 5-15z"/>
<path fill="#1d524a" d="m110 52 2-5 9-14c7-8 19-14 29-18l10-3 12-6 1-1 1 1c-3 1-3 2-4 4l-11 14-5 5v1c-11 11-22 24-31 37l-5 6-6 11h1l6 12c3 7 8 13 14 17l-6 11-6 2-24 9 2-4c11-16 14-36 11-56l-3 1-9 5 1-3 9-21 2-5z"/>
<path fill="#49b63b" d="m110 52 2-5 9-14c7-8 19-14 29-18l10-3 12-6 1-1 1 1c-3 1-3 2-4 4l-11 14-5 5-8 7-13 11-5 6c-5-2-10-5-15-3l-3 2z"/>
<path fill="#2d913b" d="m110 52 3-2c5-2 10 1 15 3l-12 13-6 9-3 1-9 5 1-3 9-21 2-5z"/>
<path fill="#2d6f4d" d="M121 92c10-5 19-9 28-17l7-6c-1 2-6 8-6 10l-4 7-13 27c-6-4-11-10-14-17l-6-12 4-5 4 13z"/>
<path fill="#49b63b" d="m154 30-1 11c0 4-3 9-6 12-6 9-19 10-25 19-1 0-3 2-4 1l5-6c9-13 20-26 31-37z"/>
<path fill="url(#d)" d="m230 135 3 6 11 17 2 5 4 5 3 6 9 14 19 36 4 5c-2 2-19 2-22 2l-5-1h-2c-12 0-25-4-35-10l-4-3c-10-8-15-16-21-27l-7-18v-1l11-7 4-2c7-5 14-12 19-19l7-8z"/>
<path fill="#1d524a" d="m230 135 3 6 11 17 2 5 4 5 3 6h-1c-10-8-20-9-32-4l23 23 13 12 1 2-20-14-13-12-4-3-5-6c-2 0-2 3-2 5l-1 10c-1 8-2 11 1 19l2 6 2 5c-10-8-15-16-21-27l-7-18v-1l11-7 4-2c7-5 14-12 19-19l7-8z"/>
<path fill="#2d6f4d" d="m233 141 11 17 2 5 4 5 3 6h-1c-10-8-20-9-32-4l-7-9 15-15 5-5zm-16 76-2-5-2-6c-3-8-2-11-1-19l1-10c0-2 0-5 2-5l5 6 4 3h-1c0 4 8 22 11 26 6 10 15 18 25 22 2 0 3 0 4 2l-5-1h-2c-12 0-25-4-35-10l-4-3z"/>
<path fill="#2d913b" d="M110 75c3 20 0 40-11 56l-2 4a145 145 0 0 0-43 31l-5 4-11 15v-7l-1-10a143 143 0 0 1 5-29l9-20c13-18 32-29 52-37l4-3 3-4z"/>
<path fill="url(#e)" d="m42 139 9-20c13-18 32-29 52-37l4-3-3 7c-4 5-8 11-13 15l-3 3c-6 6-13 12-18 19l-8 11-20 5z"/>
<path fill="#1d524a" d="M91 101c0 2-4 6-6 8-5 5-10 11-14 18l-9 13-4 9 5 1c4 0 16-9 19-7-10 7-20 14-28 23l-5 4-11 15v-7l-1-10a143 143 0 0 1 5-29l20-5 8-11c5-7 12-13 18-19l3-3z"/>
<path fill="#2d6f4d" d="m62 134-9 16-3 7-2 4c-3 3-8 5-10 8l-1-1a143 143 0 0 1 5-29l20-5zm0 6 11-5 7-4c5-1 14-2 19 0l-2 4-15 8c-3-2-15 7-19 7l-5-1 4-9z"/>
</g>
</svg>
</svg>
`;
//#endregion
//#region ../../src/cli/core/templates/mainBrowserTs.ts
const mainBrowserTs = () => `
import { Alepha, run } from "alepha";
import { WebModule } from "./web/index.ts";
const alepha = Alepha.create();
alepha.with(WebModule);
run(alepha);
`.trim();
//#endregion
//#region ../../src/cli/core/templates/mainCss.ts
const mainCss = (opts = {}) => {
if (opts.tailwind) return `@import "tailwindcss";
/* Add your styles here */
`;
return `/**
* Global styles for your application.
*
* Options:
* - Tailwind CSS: Use \`alepha init --tailwind\` to add Tailwind CSS
* - Raw CSS: Write your own styles below
*/
/* Add your styles here */
`;
};
//#endregion
//#region ../../src/cli/core/templates/mainServerTs.ts
const mainServerTs = (options = {}) => {
const { api = false, react = false } = options;
const imports = [];
const withs = [];
if (api) {
imports.push(`import { ApiModule } from "./api/index.ts";`);
withs.push(`alepha.with(ApiModule);`);
}
if (react) {
imports.push(`import { WebModule } from "./web/index.ts";`);
withs.push(`alepha.with(WebModule);`);
}
return `
import { Alepha, run } from "alepha";
${imports.length > 0 ? `${imports.join("\n")}\n` : ""}
const alepha = Alepha.create();
${withs.length > 0 ? `\n${withs.join("\n")}` : ""}
run(alepha);
`.trim();
};
//#endregion
//#region ../../src/cli/core/templates/tsconfigJson.ts
const tsconfigJson = () => `
{
"extends": "alepha/tsconfig.base",
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
}
}
`.trim();
//#endregion
//#region ../../src/cli/core/templates/viteConfigTs.ts
const viteConfigTs = () => {
return `import tailwindcss from "@tailwindcss/vite";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [tailwindcss()],
});
`;
};
//#endregion
//#region ../../src/cli/core/templates/vitestConfigTs.ts
/**
* Minimal Vitest config for a freshly scaffolded Alepha project.
*
* Defines `test.root` explicitly so Vitest doesn't walk up the directory
* tree and inherit a parent monorepo config (which can pull in unrelated
* setup files, database connections, etc.).
*/
const vitestConfigTs = () => `import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
root: ".",
globals: true,
},
});
`;
//#endregion
//#region ../../src/cli/core/templates/vscodeSettingsJson.ts
/**
* `.vscode/settings.json` — points the editor's TypeScript language server
* at the `typescript` copy embedded in `alepha`'s dependencies, so the IDE
* type-checks with the exact same compiler version as `alepha typecheck`.
*
* Without this, VS Code falls back to its own bundled TypeScript, which
* drifts from whatever `alepha` ships — the classic "green in CI, red
* squiggles in the editor" skew.
*
* The path assumes a hoisting package manager (yarn node-modules / npm /
* bun), where `alepha`