Merge pull request #5 from emdash-cms/feat/create-emdash-ux

feat(create-emdash): improve CLI branding and UX
This commit is contained in:
Matt Kane
2026-04-01 13:31:09 +01:00
committed by GitHub
12 changed files with 334 additions and 229 deletions

View File

@@ -0,0 +1,5 @@
---
"create-emdash": patch
---
Improve create-emdash CLI experience: add the EmDash branded banner, let users pick their package manager (auto-detects the one that invoked it), and ask whether to install dependencies with a spinner showing progress.

View File

@@ -18,6 +18,17 @@ const PROJECT_NAME_PATTERN = /^[a-z0-9-]+$/;
const GITHUB_REPO = "emdash-cms/templates"; const GITHUB_REPO = "emdash-cms/templates";
type PackageManager = "pnpm" | "npm" | "yarn" | "bun";
/** Detect which package manager invoked us, or fall back to npm */
function detectPackageManager(): PackageManager {
const agent = process.env.npm_config_user_agent ?? "";
if (agent.startsWith("pnpm")) return "pnpm";
if (agent.startsWith("yarn")) return "yarn";
if (agent.startsWith("bun")) return "bun";
return "npm";
}
type Platform = "node" | "cloudflare"; type Platform = "node" | "cloudflare";
interface TemplateConfig { interface TemplateConfig {
@@ -95,7 +106,8 @@ function selectOptions<K extends string>(
async function main() { async function main() {
console.clear(); console.clear();
p.intro(`${pc.bgCyan(pc.black(" create-emdash "))}`); console.log(`\n ${pc.bold(pc.cyan("— E M D A S H —"))}\n`);
p.intro("Create a new EmDash project");
const projectName = await p.text({ const projectName = await p.text({
message: "Project name?", message: "Project name?",
@@ -132,16 +144,16 @@ async function main() {
const platform = await p.select<Platform>({ const platform = await p.select<Platform>({
message: "Where will you deploy?", message: "Where will you deploy?",
options: [ options: [
{
value: "node",
label: "Node.js",
hint: "SQLite + local file storage",
},
{ {
value: "cloudflare", value: "cloudflare",
label: "Cloudflare Workers", label: "Cloudflare Workers",
hint: "D1 + R2", hint: "D1 + R2",
}, },
{
value: "node",
label: "Node.js",
hint: "SQLite + local file storage",
},
], ],
initialValue: "node", initialValue: "node",
}); });
@@ -175,6 +187,38 @@ async function main() {
? NODE_TEMPLATES[templateKey as NodeTemplate] ? NODE_TEMPLATES[templateKey as NodeTemplate]
: CLOUDFLARE_TEMPLATES[templateKey as CloudflareTemplate]; : CLOUDFLARE_TEMPLATES[templateKey as CloudflareTemplate];
// Step 3: pick package manager
const detectedPm = detectPackageManager();
const pm = await p.select<PackageManager>({
message: "Which package manager?",
options: [
{ value: "pnpm", label: "pnpm" },
{ value: "npm", label: "npm" },
{ value: "yarn", label: "yarn" },
{ value: "bun", label: "bun" },
],
initialValue: detectedPm,
});
if (p.isCancel(pm)) {
p.cancel("Operation cancelled.");
process.exit(0);
}
// Step 4: install dependencies?
const shouldInstall = await p.confirm({
message: "Install dependencies?",
initialValue: true,
});
if (p.isCancel(shouldInstall)) {
p.cancel("Operation cancelled.");
process.exit(0);
}
const installCmd = `${pm} install`;
const runCmd = (script: string) => (pm === "npm" ? `npm run ${script}` : `${pm} ${script}`);
const s = p.spinner(); const s = p.spinner();
s.start("Creating project..."); s.start("Creating project...");
@@ -204,19 +248,25 @@ async function main() {
s.stop("Project created!"); s.stop("Project created!");
s.start("Installing dependencies..."); if (shouldInstall) {
try { s.start(`Installing dependencies with ${pc.cyan(pm)}...`);
execSync("pnpm install", { try {
cwd: projectDir, execSync(installCmd, {
stdio: "ignore", cwd: projectDir,
}); stdio: "ignore",
s.stop("Dependencies installed!"); });
} catch { s.stop("Dependencies installed!");
s.stop("Failed to install dependencies"); } catch {
p.log.warn(`Run ${pc.cyan(`cd ${projectName} && pnpm install`)} manually`); s.stop("Failed to install dependencies");
p.log.warn(`Run ${pc.cyan(`cd ${projectName} && ${installCmd}`)} manually`);
}
} }
p.note(`cd ${projectName}\npnpm run bootstrap\npnpm run dev`, "Next steps"); const steps = [`cd ${projectName}`];
if (!shouldInstall) steps.push(installCmd);
steps.push(runCmd("bootstrap"), runCmd("dev"));
p.note(steps.join("\n"), "Next steps");
p.outro(`${pc.green("Done!")} Your EmDash project is ready at ${pc.cyan(projectName)}`); p.outro(`${pc.green("Done!")} Your EmDash project is ready at ${pc.cyan(projectName)}`);
} catch (error) { } catch (error) {

View File

@@ -1,43 +1,48 @@
{ {
"name": "@emdash-cms/plugin-ai-moderation", "name": "@emdash-cms/plugin-ai-moderation",
"version": "0.0.2", "version": "0.0.2",
"description": "AI-powered comment moderation plugin for EmDash CMS using Cloudflare Workers AI (Llama Guard)", "description": "AI-powered comment moderation plugin for EmDash CMS using Cloudflare Workers AI (Llama Guard)",
"type": "module", "type": "module",
"main": "src/descriptor.ts", "main": "src/descriptor.ts",
"exports": { "exports": {
".": "./src/descriptor.ts", ".": "./src/descriptor.ts",
"./plugin": "./src/index.ts", "./plugin": "./src/index.ts",
"./admin": "./src/admin.tsx" "./admin": "./src/admin.tsx"
}, },
"files": [ "files": [
"src" "src"
], ],
"keywords": [ "keywords": [
"emdash", "emdash",
"cms", "cms",
"plugin", "plugin",
"ai", "ai",
"moderation", "moderation",
"comments", "comments",
"llama-guard" "llama-guard"
], ],
"author": "Matt Kane", "author": "Matt Kane",
"license": "MIT", "license": "MIT",
"peerDependencies": { "peerDependencies": {
"emdash": "workspace:*", "emdash": "workspace:*",
"react": "^18.0.0 || ^19.0.0", "react": "^18.0.0 || ^19.0.0",
"@phosphor-icons/react": "^2.1.10", "@phosphor-icons/react": "^2.1.10",
"@cloudflare/kumo": "^1.0.0" "@cloudflare/kumo": "^1.0.0"
}, },
"devDependencies": { "devDependencies": {
"@cloudflare/workers-types": "^4.20250224.0", "@cloudflare/workers-types": "^4.20250224.0",
"@types/react": "catalog:", "@types/react": "catalog:",
"vitest": "catalog:" "vitest": "catalog:"
}, },
"scripts": { "scripts": {
"test": "vitest run", "test": "vitest run",
"typecheck": "tsgo --noEmit" "typecheck": "tsgo --noEmit"
}, },
"dependencies": {}, "dependencies": {},
"optionalDependencies": {} "optionalDependencies": {},
"repository": {
"type": "git",
"url": "git+https://github.com/emdash-cms/emdash.git",
"directory": "packages/plugins/ai-moderation"
}
} }

View File

@@ -1,32 +1,37 @@
{ {
"name": "@emdash-cms/plugin-api-test", "name": "@emdash-cms/plugin-api-test",
"private": true, "private": true,
"version": "0.0.2", "version": "0.0.2",
"description": "Test plugin that exercises all EmDash plugin APIs", "description": "Test plugin that exercises all EmDash plugin APIs",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"exports": { "exports": {
".": "./src/index.ts", ".": "./src/index.ts",
"./admin": "./src/admin.tsx" "./admin": "./src/admin.tsx"
}, },
"files": [ "files": [
"src" "src"
], ],
"keywords": [ "keywords": [
"emdash", "emdash",
"cms", "cms",
"plugin", "plugin",
"test", "test",
"api" "api"
], ],
"author": "Matt Kane", "author": "Matt Kane",
"license": "MIT", "license": "MIT",
"peerDependencies": { "peerDependencies": {
"emdash": "workspace:*", "emdash": "workspace:*",
"react": "^18.0.0 || ^19.0.0", "react": "^18.0.0 || ^19.0.0",
"@phosphor-icons/react": "^2.1.10" "@phosphor-icons/react": "^2.1.10"
}, },
"dependencies": {}, "dependencies": {},
"devDependencies": {}, "devDependencies": {},
"optionalDependencies": {} "optionalDependencies": {},
} "repository": {
"type": "git",
"url": "git+https://github.com/emdash-cms/emdash.git",
"directory": "packages/plugins/api-test"
}
}

View File

@@ -32,5 +32,10 @@
"scripts": { "scripts": {
"test": "vitest run", "test": "vitest run",
"typecheck": "tsgo --noEmit" "typecheck": "tsgo --noEmit"
},
"repository": {
"type": "git",
"url": "git+https://github.com/emdash-cms/emdash.git",
"directory": "packages/plugins/atproto"
} }
} }

View File

@@ -1,33 +1,38 @@
{ {
"name": "@emdash-cms/plugin-audit-log", "name": "@emdash-cms/plugin-audit-log",
"version": "0.0.2", "version": "0.0.2",
"description": "Audit logging plugin for EmDash CMS - tracks content changes", "description": "Audit logging plugin for EmDash CMS - tracks content changes",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"exports": { "exports": {
".": "./src/index.ts", ".": "./src/index.ts",
"./sandbox": "./src/sandbox-entry.ts" "./sandbox": "./src/sandbox-entry.ts"
}, },
"files": [ "files": [
"src" "src"
], ],
"keywords": [ "keywords": [
"emdash", "emdash",
"cms", "cms",
"plugin", "plugin",
"audit", "audit",
"logging", "logging",
"history" "history"
], ],
"author": "Matt Kane", "author": "Matt Kane",
"license": "MIT", "license": "MIT",
"dependencies": {}, "dependencies": {},
"peerDependencies": { "peerDependencies": {
"emdash": "workspace:*" "emdash": "workspace:*"
}, },
"devDependencies": {}, "devDependencies": {},
"scripts": { "scripts": {
"typecheck": "tsgo --noEmit" "typecheck": "tsgo --noEmit"
}, },
"optionalDependencies": {} "optionalDependencies": {},
} "repository": {
"type": "git",
"url": "git+https://github.com/emdash-cms/emdash.git",
"directory": "packages/plugins/audit-log"
}
}

View File

@@ -30,5 +30,10 @@
}, },
"scripts": { "scripts": {
"typecheck": "tsgo --noEmit" "typecheck": "tsgo --noEmit"
},
"repository": {
"type": "git",
"url": "git+https://github.com/emdash-cms/emdash.git",
"directory": "packages/plugins/color"
} }
} }

View File

@@ -1,37 +1,42 @@
{ {
"name": "@emdash-cms/plugin-embeds", "name": "@emdash-cms/plugin-embeds",
"version": "0.0.2", "version": "0.0.2",
"description": "Embed blocks for EmDash CMS - YouTube, Vimeo, Twitter, Bluesky, Mastodon, and more", "description": "Embed blocks for EmDash CMS - YouTube, Vimeo, Twitter, Bluesky, Mastodon, and more",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"exports": { "exports": {
".": "./src/index.ts", ".": "./src/index.ts",
"./astro": "./src/astro/index.ts" "./astro": "./src/astro/index.ts"
}, },
"files": [ "files": [
"src" "src"
], ],
"keywords": [ "keywords": [
"emdash", "emdash",
"cms", "cms",
"plugin", "plugin",
"embed", "embed",
"youtube", "youtube",
"vimeo", "vimeo",
"twitter", "twitter",
"bluesky" "bluesky"
], ],
"author": "Matt Kane", "author": "Matt Kane",
"license": "MIT", "license": "MIT",
"peerDependencies": { "peerDependencies": {
"astro": ">=6.0.0-beta.0", "astro": ">=6.0.0-beta.0",
"emdash": "workspace:*" "emdash": "workspace:*"
}, },
"dependencies": { "dependencies": {
"@emdash-cms/blocks": "workspace:*", "@emdash-cms/blocks": "workspace:*",
"astro-embed": "^0.12.0" "astro-embed": "^0.12.0"
}, },
"scripts": { "scripts": {
"typecheck": "tsgo --noEmit" "typecheck": "tsgo --noEmit"
} },
} "repository": {
"type": "git",
"url": "git+https://github.com/emdash-cms/emdash.git",
"directory": "packages/plugins/embeds"
}
}

View File

@@ -36,5 +36,10 @@
}, },
"scripts": { "scripts": {
"typecheck": "tsgo --noEmit" "typecheck": "tsgo --noEmit"
},
"repository": {
"type": "git",
"url": "git+https://github.com/emdash-cms/emdash.git",
"directory": "packages/plugins/forms"
} }
} }

View File

@@ -35,5 +35,10 @@
"devDependencies": { "devDependencies": {
"tsdown": "catalog:", "tsdown": "catalog:",
"typescript": "catalog:" "typescript": "catalog:"
},
"repository": {
"type": "git",
"url": "git+https://github.com/emdash-cms/emdash.git",
"directory": "packages/plugins/marketplace-test"
} }
} }

View File

@@ -1,41 +1,46 @@
{ {
"name": "@emdash-cms/plugin-sandboxed-test", "name": "@emdash-cms/plugin-sandboxed-test",
"private": true, "private": true,
"version": "0.0.2", "version": "0.0.2",
"description": "Test plugin for sandboxed plugin system", "description": "Test plugin for sandboxed plugin system",
"type": "module", "type": "module",
"main": "dist/index.mjs", "main": "dist/index.mjs",
"exports": { "exports": {
".": { ".": {
"import": "./dist/index.mjs", "import": "./dist/index.mjs",
"types": "./dist/index.d.mts" "types": "./dist/index.d.mts"
}, },
"./sandbox": "./dist/sandbox-entry.mjs" "./sandbox": "./dist/sandbox-entry.mjs"
}, },
"files": [ "files": [
"dist" "dist"
], ],
"scripts": { "scripts": {
"build": "tsdown src/index.ts src/sandbox-entry.ts --format esm --dts --clean", "build": "tsdown src/index.ts src/sandbox-entry.ts --format esm --dts --clean",
"dev": "tsdown src/index.ts src/sandbox-entry.ts --format esm --dts --watch", "dev": "tsdown src/index.ts src/sandbox-entry.ts --format esm --dts --watch",
"typecheck": "tsgo --noEmit" "typecheck": "tsgo --noEmit"
}, },
"keywords": [ "keywords": [
"emdash", "emdash",
"cms", "cms",
"plugin", "plugin",
"test", "test",
"sandbox" "sandbox"
], ],
"author": "Matt Kane", "author": "Matt Kane",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"emdash": "workspace:*" "emdash": "workspace:*"
}, },
"devDependencies": { "devDependencies": {
"tsdown": "catalog:", "tsdown": "catalog:",
"typescript": "catalog:" "typescript": "catalog:"
}, },
"peerDependencies": {}, "peerDependencies": {},
"optionalDependencies": {} "optionalDependencies": {},
} "repository": {
"type": "git",
"url": "git+https://github.com/emdash-cms/emdash.git",
"directory": "packages/plugins/sandboxed-test"
}
}

View File

@@ -1,33 +1,38 @@
{ {
"name": "@emdash-cms/plugin-webhook-notifier", "name": "@emdash-cms/plugin-webhook-notifier",
"version": "0.0.2", "version": "0.0.2",
"description": "Webhook notification plugin for EmDash CMS - posts to external URLs on content changes", "description": "Webhook notification plugin for EmDash CMS - posts to external URLs on content changes",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"exports": { "exports": {
".": "./src/index.ts", ".": "./src/index.ts",
"./sandbox": "./src/sandbox-entry.ts" "./sandbox": "./src/sandbox-entry.ts"
}, },
"files": [ "files": [
"src" "src"
], ],
"keywords": [ "keywords": [
"emdash", "emdash",
"cms", "cms",
"plugin", "plugin",
"webhook", "webhook",
"notifications", "notifications",
"integration" "integration"
], ],
"author": "Matt Kane", "author": "Matt Kane",
"license": "MIT", "license": "MIT",
"peerDependencies": { "peerDependencies": {
"emdash": "workspace:*" "emdash": "workspace:*"
}, },
"devDependencies": {}, "devDependencies": {},
"scripts": { "scripts": {
"typecheck": "tsgo --noEmit" "typecheck": "tsgo --noEmit"
}, },
"dependencies": {}, "dependencies": {},
"optionalDependencies": {} "optionalDependencies": {},
} "repository": {
"type": "git",
"url": "git+https://github.com/emdash-cms/emdash.git",
"directory": "packages/plugins/webhook-notifier"
}
}