create-composure-app 1.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.
Files changed (45) hide show
  1. package/dist/commands/create.d.ts +8 -0
  2. package/dist/commands/create.js +89 -0
  3. package/dist/index.d.ts +2 -0
  4. package/dist/index.js +17 -0
  5. package/dist/lib/logger.d.ts +6 -0
  6. package/dist/lib/logger.js +7 -0
  7. package/dist/lib/prompts.d.ts +9 -0
  8. package/dist/lib/prompts.js +85 -0
  9. package/dist/steps/download-template.d.ts +1 -0
  10. package/dist/steps/download-template.js +38 -0
  11. package/dist/steps/init-git.d.ts +1 -0
  12. package/dist/steps/init-git.js +12 -0
  13. package/dist/steps/install-deps.d.ts +1 -0
  14. package/dist/steps/install-deps.js +6 -0
  15. package/dist/steps/print-next-steps.d.ts +6 -0
  16. package/dist/steps/print-next-steps.js +39 -0
  17. package/dist/steps/reset-migrations.d.ts +1 -0
  18. package/dist/steps/reset-migrations.js +23 -0
  19. package/dist/steps/rewrite-package-names.d.ts +9 -0
  20. package/dist/steps/rewrite-package-names.js +51 -0
  21. package/dist/steps/rewrite-package-names.test.d.ts +1 -0
  22. package/dist/steps/rewrite-package-names.test.js +32 -0
  23. package/dist/steps/scaffold-tier.d.ts +13 -0
  24. package/dist/steps/scaffold-tier.js +141 -0
  25. package/dist/steps/validate-license.d.ts +17 -0
  26. package/dist/steps/validate-license.js +135 -0
  27. package/dist/steps/validate-target.d.ts +1 -0
  28. package/dist/steps/validate-target.js +14 -0
  29. package/package.json +33 -0
  30. package/src/commands/create.ts +124 -0
  31. package/src/index.ts +22 -0
  32. package/src/lib/logger.ts +8 -0
  33. package/src/lib/prompts.ts +114 -0
  34. package/src/steps/download-template.ts +55 -0
  35. package/src/steps/init-git.ts +18 -0
  36. package/src/steps/install-deps.ts +7 -0
  37. package/src/steps/print-next-steps.ts +60 -0
  38. package/src/steps/reset-migrations.ts +27 -0
  39. package/src/steps/rewrite-package-names.test.ts +35 -0
  40. package/src/steps/rewrite-package-names.ts +59 -0
  41. package/src/steps/scaffold-tier.ts +172 -0
  42. package/src/steps/validate-license.ts +181 -0
  43. package/src/steps/validate-target.ts +21 -0
  44. package/tests/e2e.test.ts +57 -0
  45. package/tsconfig.json +16 -0
@@ -0,0 +1,135 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { join } from "node:path";
3
+ import { homedir } from "node:os";
4
+ import { existsSync } from "node:fs";
5
+ const COMPOSURE_DIR = join(homedir(), ".composure");
6
+ const CREDENTIALS_PATH = join(COMPOSURE_DIR, "credentials.json");
7
+ const TOKEN_SCRIPT = join(COMPOSURE_DIR, "bin", "composure-token.mjs");
8
+ /**
9
+ * Validate license via Composure's auth system.
10
+ *
11
+ * Priority:
12
+ * 1. --token flag (legacy, still supported)
13
+ * 2. ~/.composure/credentials.json (Composure login)
14
+ *
15
+ * Returns plan info used to determine available tiers.
16
+ */
17
+ export async function validateLicense(token) {
18
+ // Method 1: Composure credentials (preferred)
19
+ if (existsSync(CREDENTIALS_PATH)) {
20
+ return validateViaComposure();
21
+ }
22
+ // Method 2: Legacy token flag
23
+ if (token) {
24
+ return validateViaToken(token);
25
+ }
26
+ throw new Error("Not authenticated. Log in with Composure first:\n" +
27
+ " Run: /composure:auth login\n\n" +
28
+ "Or use a license token:\n" +
29
+ " npx create-composure-app --token=<your-token>\n\n" +
30
+ "Get a license at https://composure-pro.com/template");
31
+ }
32
+ /**
33
+ * Validate using Composure's composure-token.mjs CLI
34
+ */
35
+ async function validateViaComposure() {
36
+ // Check if composure-token.mjs exists
37
+ if (!existsSync(TOKEN_SCRIPT)) {
38
+ throw new Error("Composure CLI not found. Install Composure first:\n" +
39
+ " claude plugin install composure@composure-suite\n" +
40
+ " Then: /composure:auth login");
41
+ }
42
+ try {
43
+ // Step 1: Validate token is current (handles refresh automatically)
44
+ const validateOutput = execFileSync("node", [TOKEN_SCRIPT, "validate"], {
45
+ encoding: "utf8",
46
+ timeout: 15000,
47
+ }).trim();
48
+ // Output format: "valid:{plan}:{email}" or "expired" or "not-authenticated"
49
+ if (validateOutput === "not-authenticated") {
50
+ throw new Error("Not authenticated. Run: /composure:auth login");
51
+ }
52
+ if (validateOutput === "expired") {
53
+ throw new Error("Session expired. Run: /composure:auth login to refresh.");
54
+ }
55
+ const parts = validateOutput.split(":");
56
+ if (parts[0] !== "valid" || parts.length < 3) {
57
+ throw new Error(`Unexpected validation response: ${validateOutput}`);
58
+ }
59
+ const plan = normalizePlan(parts[1]);
60
+ const email = parts.slice(2).join(":"); // email may contain colons (unlikely but safe)
61
+ // Step 2: Get full license info (features, etc.)
62
+ let features = [];
63
+ try {
64
+ const licenseOutput = execFileSync("node", [TOKEN_SCRIPT, "license"], { encoding: "utf8", timeout: 15000 }).trim();
65
+ const licenseData = JSON.parse(licenseOutput);
66
+ features = licenseData.features ?? [];
67
+ }
68
+ catch {
69
+ // License endpoint unavailable — proceed with plan-based features
70
+ features = getDefaultFeatures(plan);
71
+ }
72
+ return { valid: true, plan, email, features };
73
+ }
74
+ catch (error) {
75
+ if (error instanceof Error && error.message.includes("Not authenticated")) {
76
+ throw error;
77
+ }
78
+ if (error instanceof Error && error.message.includes("Session expired")) {
79
+ throw error;
80
+ }
81
+ throw new Error(`License validation failed: ${error instanceof Error ? error.message : String(error)}`);
82
+ }
83
+ }
84
+ /**
85
+ * Validate using legacy token (standalone license server)
86
+ */
87
+ async function validateViaToken(token) {
88
+ const LICENSE_SERVER = process.env.TEMPLATE_LICENSE_SERVER ?? "https://composure-pro.com";
89
+ const response = await fetch(`${LICENSE_SERVER}/api/v1/license/validate`, {
90
+ headers: {
91
+ Authorization: `Bearer ${token}`,
92
+ "Content-Type": "application/json",
93
+ },
94
+ });
95
+ if (!response.ok) {
96
+ const body = await response
97
+ .json()
98
+ .catch(() => ({ message: "Unknown error" }));
99
+ throw new Error(`License validation failed: ${body.message ?? response.statusText}`);
100
+ }
101
+ const data = (await response.json());
102
+ return {
103
+ valid: true,
104
+ plan: normalizePlan(data.plan),
105
+ email: data.email ?? "unknown",
106
+ features: data.features ?? getDefaultFeatures(normalizePlan(data.plan)),
107
+ };
108
+ }
109
+ /**
110
+ * Normalize plan string to TemplatePlan
111
+ */
112
+ function normalizePlan(plan) {
113
+ switch (plan?.toLowerCase()) {
114
+ case "enterprise":
115
+ return "enterprise";
116
+ case "pro":
117
+ return "pro";
118
+ default:
119
+ return "free";
120
+ }
121
+ }
122
+ /**
123
+ * Default features by plan when license endpoint doesn't return them
124
+ */
125
+ function getDefaultFeatures(plan) {
126
+ switch (plan) {
127
+ case "enterprise":
128
+ return ["starter", "pro", "mobile", "support"];
129
+ case "pro":
130
+ return ["starter", "pro", "mobile"];
131
+ case "free":
132
+ default:
133
+ return ["starter", "mobile"];
134
+ }
135
+ }
@@ -0,0 +1 @@
1
+ export declare function validateTarget(projectName: string): Promise<void>;
@@ -0,0 +1,14 @@
1
+ import { existsSync, readdirSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ export async function validateTarget(projectName) {
4
+ if (!/^[a-z0-9-_]+$/i.test(projectName)) {
5
+ throw new Error(`Invalid project name "${projectName}". Use letters, numbers, dashes, and underscores only.`);
6
+ }
7
+ const targetPath = resolve(process.cwd(), projectName);
8
+ if (existsSync(targetPath)) {
9
+ const contents = readdirSync(targetPath);
10
+ if (contents.length > 0) {
11
+ throw new Error(`Target directory "${projectName}" exists and is not empty. Refusing to overwrite.`);
12
+ }
13
+ }
14
+ }
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "create-composure-app",
3
+ "version": "1.0.0",
4
+ "description": "Scaffold a new Composure Template project — multi-tenant Next.js + Supabase + Expo",
5
+ "type": "module",
6
+ "bin": {
7
+ "create-composure-app": "./dist/index.js"
8
+ },
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "dev": "tsc --watch",
12
+ "test": "vitest run",
13
+ "prepublishOnly": "pnpm build"
14
+ },
15
+ "dependencies": {
16
+ "commander": "^13.1.0",
17
+ "kleur": "^4.1.5",
18
+ "ora": "^8.2.0",
19
+ "prompts": "^2.4.2"
20
+ },
21
+ "devDependencies": {
22
+ "@types/node": "^22.0.0",
23
+ "@types/prompts": "^2.4.9",
24
+ "typescript": "^5.9.3",
25
+ "vitest": "^4.1.2"
26
+ },
27
+ "engines": {
28
+ "node": ">=22.0.0"
29
+ },
30
+ "publishConfig": {
31
+ "access": "public"
32
+ }
33
+ }
@@ -0,0 +1,124 @@
1
+ import kleur from "kleur";
2
+ import ora from "ora";
3
+ import { validateTarget } from "../steps/validate-target.js";
4
+ import { validateLicense } from "../steps/validate-license.js";
5
+ import { downloadTemplate } from "../steps/download-template.js";
6
+ import { rewritePackageNames } from "../steps/rewrite-package-names.js";
7
+ import { resetMigrations } from "../steps/reset-migrations.js";
8
+ import { scaffoldTier } from "../steps/scaffold-tier.js";
9
+ import { initGit } from "../steps/init-git.js";
10
+ import { installDeps } from "../steps/install-deps.js";
11
+ import { printNextSteps } from "../steps/print-next-steps.js";
12
+ import { promptProjectName, promptInstallTier } from "../lib/prompts.js";
13
+ import type { InstallTier } from "../lib/prompts.js";
14
+ import { logger } from "../lib/logger.js";
15
+
16
+ export interface CreateOptions {
17
+ token?: string;
18
+ cleanMigrations?: boolean;
19
+ skipInstall?: boolean;
20
+ dev?: boolean;
21
+ tier?: string;
22
+ }
23
+
24
+ export async function createCommand(
25
+ name: string | undefined,
26
+ options: CreateOptions,
27
+ ) {
28
+ logger.info(kleur.bold().blue("\n create-composure-app\n"));
29
+
30
+ try {
31
+ const projectName = name ?? (await promptProjectName());
32
+
33
+ await validateTarget(projectName);
34
+
35
+ // ── License validation ──────────────────────────────────
36
+ let tier: InstallTier;
37
+
38
+ if (options.dev) {
39
+ logger.warn("Dev mode — skipping license validation");
40
+ // In dev mode, allow tier flag or prompt
41
+ tier = (options.tier as InstallTier) ?? (await promptInstallTier("enterprise"));
42
+ } else {
43
+ const spinner = ora("Validating license...").start();
44
+ const license = await validateLicense(options.token);
45
+ spinner.succeed(
46
+ `License validated — ${kleur.green(license.plan)} plan (${license.email})`,
47
+ );
48
+
49
+ // ── Interactive tier selection ──────────────────────────
50
+ logger.info("");
51
+ tier = (options.tier as InstallTier) ?? (await promptInstallTier(license.plan));
52
+ }
53
+
54
+ const includeMobile = tier === "starter-mobile" || tier === "pro-mobile";
55
+ const includePro = tier === "pro" || tier === "pro-mobile";
56
+
57
+ logger.info(
58
+ `\n ${kleur.bold("Selected:")} ${tierLabel(tier)}\n`,
59
+ );
60
+
61
+ // ── Download ──────────────────────────────────────────────
62
+ const downloadSpinner = ora("Downloading template...").start();
63
+ await downloadTemplate(projectName, options.token, options.dev);
64
+ downloadSpinner.succeed("Template downloaded");
65
+
66
+ // ── Scaffold tier (remove unused files) ──────────────────
67
+ const scaffoldSpinner = ora("Scaffolding for selected tier...").start();
68
+ const { removed } = await scaffoldTier(projectName, tier);
69
+ if (removed.length > 0) {
70
+ scaffoldSpinner.succeed(
71
+ `Scaffolded — removed ${removed.length} items not in ${tierLabel(tier)}`,
72
+ );
73
+ } else {
74
+ scaffoldSpinner.succeed("Full template installed");
75
+ }
76
+
77
+ // ── Personalize ──────────────────────────────────────────
78
+ const rewriteSpinner = ora("Personalizing project...").start();
79
+ await rewritePackageNames(projectName);
80
+ rewriteSpinner.succeed("Project personalized");
81
+
82
+ // ── Migrations ───────────────────────────────────────────
83
+ if (options.cleanMigrations) {
84
+ const migSpinner = ora("Resetting migrations...").start();
85
+ await resetMigrations(projectName);
86
+ migSpinner.succeed("Migrations reset");
87
+ }
88
+
89
+ // ── Git ──────────────────────────────────────────────────
90
+ const gitSpinner = ora("Initializing git...").start();
91
+ await initGit(projectName);
92
+ gitSpinner.succeed("Git initialized");
93
+
94
+ // ── Dependencies ─────────────────────────────────────────
95
+ if (!options.skipInstall) {
96
+ const installSpinner = ora(
97
+ "Installing dependencies (this may take a minute)...",
98
+ ).start();
99
+ await installDeps(projectName);
100
+ installSpinner.succeed("Dependencies installed");
101
+ }
102
+
103
+ // ── Summary ──────────────────────────────────────────────
104
+ printNextSteps(projectName, { includeMobile, includePro });
105
+ } catch (error) {
106
+ logger.error(
107
+ `\nFailed: ${error instanceof Error ? error.message : String(error)}`,
108
+ );
109
+ process.exit(1);
110
+ }
111
+ }
112
+
113
+ function tierLabel(tier: InstallTier): string {
114
+ switch (tier) {
115
+ case "starter":
116
+ return "Starter (Web)";
117
+ case "starter-mobile":
118
+ return "Starter + Mobile";
119
+ case "pro":
120
+ return "Pro (Web)";
121
+ case "pro-mobile":
122
+ return "Pro + Mobile";
123
+ }
124
+ }
package/src/index.ts ADDED
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from "commander";
4
+ import { createCommand } from "./commands/create.js";
5
+
6
+ const program = new Command();
7
+
8
+ program
9
+ .name("create-composure-app")
10
+ .description("Scaffold a new Composure Template project — multi-tenant Next.js + Supabase + Expo")
11
+ .version("1.0.0");
12
+
13
+ program
14
+ .argument("[name]", "Project name")
15
+ .option("-t, --token <token>", "OAuth license token (or use Composure login)")
16
+ .option("--tier <tier>", "Install tier: starter, starter-mobile, pro, pro-mobile")
17
+ .option("--clean-migrations", "Reset migrations to a single baseline", false)
18
+ .option("--skip-install", "Skip pnpm install step", false)
19
+ .option("--dev", "Development mode — skips license validation", false)
20
+ .action(createCommand);
21
+
22
+ program.parse();
@@ -0,0 +1,8 @@
1
+ import kleur from "kleur";
2
+
3
+ export const logger = {
4
+ info: (msg: string) => console.log(kleur.blue(msg)),
5
+ success: (msg: string) => console.log(kleur.green(msg)),
6
+ warn: (msg: string) => console.log(kleur.yellow(msg)),
7
+ error: (msg: string) => console.error(kleur.red(msg)),
8
+ };
@@ -0,0 +1,114 @@
1
+ import prompts from "prompts";
2
+ import type { TemplatePlan } from "../steps/validate-license.js";
3
+
4
+ export async function promptProjectName(): Promise<string> {
5
+ const { name } = await prompts({
6
+ type: "text",
7
+ name: "name",
8
+ message: "Project name",
9
+ validate: (value: string) =>
10
+ /^[a-z0-9-_]+$/i.test(value) || "Use letters, numbers, dashes, and underscores only",
11
+ });
12
+
13
+ if (!name) {
14
+ throw new Error("Project name is required.");
15
+ }
16
+
17
+ return name;
18
+ }
19
+
20
+ export async function promptConfirm(message: string): Promise<boolean> {
21
+ const { confirmed } = await prompts({
22
+ type: "confirm",
23
+ name: "confirmed",
24
+ message,
25
+ initial: true,
26
+ });
27
+
28
+ return confirmed ?? false;
29
+ }
30
+
31
+ // =============================================================================
32
+ // Tier Selection
33
+ // =============================================================================
34
+
35
+ export type InstallTier = "starter" | "starter-mobile" | "pro" | "pro-mobile";
36
+
37
+ interface TierOption {
38
+ title: string;
39
+ value: InstallTier;
40
+ description: string;
41
+ }
42
+
43
+ const ALL_TIERS: TierOption[] = [
44
+ {
45
+ title: "Starter (Web only)",
46
+ value: "starter",
47
+ description: "Auth, accounts, users, contacts, settings, team",
48
+ },
49
+ {
50
+ title: "Starter + Mobile",
51
+ value: "starter-mobile",
52
+ description: "Starter + Expo app with 3 tenant tab layouts",
53
+ },
54
+ {
55
+ title: "Pro (Web only)",
56
+ value: "pro",
57
+ description: "All 30 tables, all screens, CRM, inbox, workflows, AI, commerce",
58
+ },
59
+ {
60
+ title: "Pro + Mobile",
61
+ value: "pro-mobile",
62
+ description: "Everything + Expo mobile app",
63
+ },
64
+ ];
65
+
66
+ /**
67
+ * Get available tiers based on license plan.
68
+ *
69
+ * - enterprise: all tiers
70
+ * - pro: all tiers
71
+ * - free: starter tiers only
72
+ */
73
+ function getAvailableTiers(plan: TemplatePlan): TierOption[] {
74
+ switch (plan) {
75
+ case "enterprise":
76
+ case "pro":
77
+ return ALL_TIERS;
78
+ case "free":
79
+ default:
80
+ return ALL_TIERS.filter(
81
+ (t) => t.value === "starter" || t.value === "starter-mobile",
82
+ );
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Prompt the user to select their install tier.
88
+ * Enterprise users see all options. Free users see starter options only.
89
+ */
90
+ export async function promptInstallTier(
91
+ plan: TemplatePlan,
92
+ ): Promise<InstallTier> {
93
+ const available = getAvailableTiers(plan);
94
+
95
+ // If enterprise with only one sensible choice, still show the prompt
96
+ // so internal users can choose web-only vs web+mobile
97
+ const { tier } = await prompts({
98
+ type: "select",
99
+ name: "tier",
100
+ message: "What would you like to install?",
101
+ choices: available.map((t) => ({
102
+ title: t.title,
103
+ value: t.value,
104
+ description: t.description,
105
+ })),
106
+ initial: available.length - 1, // Default to the highest tier available
107
+ });
108
+
109
+ if (!tier) {
110
+ throw new Error("Installation cancelled.");
111
+ }
112
+
113
+ return tier as InstallTier;
114
+ }
@@ -0,0 +1,55 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { resolve, basename } from "node:path";
3
+
4
+ const TEMPLATE_REPO =
5
+ process.env.TEMPLATE_REPO_URL ?? "https://github.com/hrconsultnj/template-monorepo.git";
6
+
7
+ export async function downloadTemplate(
8
+ projectName: string,
9
+ token?: string,
10
+ devMode?: boolean,
11
+ ): Promise<void> {
12
+ // Defense-in-depth: ensure projectName is just a directory name, no path traversal
13
+ if (basename(projectName) !== projectName) {
14
+ throw new Error(`Invalid project name: "${projectName}" contains path separators.`);
15
+ }
16
+
17
+ const targetPath = resolve(process.cwd(), projectName);
18
+
19
+ if (devMode) {
20
+ // execFileSync bypasses the shell — no injection possible
21
+ execFileSync("git", [
22
+ "clone", "--depth=1", "--single-branch", TEMPLATE_REPO, targetPath,
23
+ ], { stdio: "pipe" });
24
+ return;
25
+ }
26
+
27
+ // Production mode: download tarball from license server
28
+ const LICENSE_SERVER =
29
+ process.env.TEMPLATE_LICENSE_SERVER ?? "https://composure-pro.com";
30
+
31
+ const response = await fetch(`${LICENSE_SERVER}/download`, {
32
+ headers: {
33
+ Authorization: `Bearer ${token}`,
34
+ },
35
+ });
36
+
37
+ if (!response.ok) {
38
+ throw new Error(`Download failed: ${response.statusText}`);
39
+ }
40
+
41
+ const { mkdirSync, writeFileSync } = await import("node:fs");
42
+ mkdirSync(targetPath, { recursive: true });
43
+
44
+ const buffer = Buffer.from(await response.arrayBuffer());
45
+ const tarballPath = resolve(targetPath, ".template.tar.gz");
46
+ writeFileSync(tarballPath, buffer);
47
+
48
+ // --no-absolute-names prevents path traversal in tarball entries
49
+ execFileSync("tar", [
50
+ "xzf", ".template.tar.gz", "--strip-components=1", "--no-absolute-names",
51
+ ], { cwd: targetPath, stdio: "pipe" });
52
+
53
+ const { unlinkSync } = await import("node:fs");
54
+ unlinkSync(tarballPath);
55
+ }
@@ -0,0 +1,18 @@
1
+ import { execSync } from "node:child_process";
2
+ import { rmSync } from "node:fs";
3
+ import { resolve } from "node:path";
4
+
5
+ export async function initGit(projectName: string): Promise<void> {
6
+ const targetPath = resolve(process.cwd(), projectName);
7
+
8
+ // Remove the cloned .git directory
9
+ rmSync(resolve(targetPath, ".git"), { recursive: true, force: true });
10
+
11
+ // Initialize fresh git repo
12
+ execSync("git init", { cwd: targetPath, stdio: "pipe" });
13
+ execSync("git add -A", { cwd: targetPath, stdio: "pipe" });
14
+ execSync(
15
+ 'git commit -m "chore: initial commit from create-composure-app"',
16
+ { cwd: targetPath, stdio: "pipe" },
17
+ );
18
+ }
@@ -0,0 +1,7 @@
1
+ import { execSync } from "node:child_process";
2
+ import { resolve } from "node:path";
3
+
4
+ export async function installDeps(projectName: string): Promise<void> {
5
+ const targetPath = resolve(process.cwd(), projectName);
6
+ execSync("pnpm install", { cwd: targetPath, stdio: "pipe" });
7
+ }
@@ -0,0 +1,60 @@
1
+ import kleur from "kleur";
2
+
3
+ interface NextStepsOptions {
4
+ includeMobile?: boolean;
5
+ includePro?: boolean;
6
+ }
7
+
8
+ export function printNextSteps(
9
+ projectName: string,
10
+ options: NextStepsOptions = {},
11
+ ): void {
12
+ const { includeMobile = false, includePro = false } = options;
13
+ const name = kleur.cyan(projectName);
14
+
15
+ const features = [
16
+ `- ${kleur.blue("Web app")} — Next.js 16 + Tailwind + shadcn`,
17
+ `- ${kleur.blue("Database")} — Supabase + typed queries + RLS`,
18
+ `- ${kleur.blue("3 tenant types")} — Admin / Workspace / Portal`,
19
+ `- ${kleur.blue("Auth")} — Contact-first signup + multi-account`,
20
+ ];
21
+
22
+ if (includeMobile) {
23
+ features.push(
24
+ `- ${kleur.blue("Mobile app")} — Expo SDK 55 + React Native`,
25
+ );
26
+ }
27
+
28
+ if (includePro) {
29
+ features.push(
30
+ `- ${kleur.blue("CRM")} — Tasks, deals, pipelines`,
31
+ `- ${kleur.blue("Inbox")} — Threads, compose, reply`,
32
+ `- ${kleur.blue("AI")} — Agents, memory graph, thinking`,
33
+ `- ${kleur.blue("Commerce")} — Invoices, quotes, payments`,
34
+ `- ${kleur.blue("Workflows")} — Visual automation engine`,
35
+ );
36
+ }
37
+
38
+ console.log(`
39
+ ${kleur.green().bold("Your project is ready!")}
40
+
41
+ ${kleur.bold("Next steps:")}
42
+ ${kleur.gray("$")} cd ${name}
43
+ ${kleur.gray("$")} pnpm supabase start
44
+ ${kleur.gray("$")} pnpm dev
45
+
46
+ ${kleur.bold("First-time setup:")}
47
+ 1. Copy ${kleur.yellow(".env.example")} to ${kleur.yellow(".env")}
48
+ 2. Fill in your Supabase keys
49
+ 3. Run ${kleur.yellow(`pnpm --filter @${projectName}/database db:types`)} after migrations apply
50
+ 4. Visit ${kleur.underline("http://localhost:3000")} to sign up
51
+
52
+ ${kleur.bold("What's included:")}
53
+ ${features.map((f) => ` ${f}`).join("\n")}
54
+ ${!includePro ? `\n${kleur.yellow("Upgrade to Pro for CRM, inbox, workflows, AI, and commerce.")}` : ""}
55
+ ${!includeMobile ? `\n${kleur.yellow("Add mobile with the + Mobile tier for Expo SDK 55 support.")}` : ""}
56
+
57
+ ${kleur.gray("Docs: https://composure-pro.com/template/docs")}
58
+ ${kleur.gray("Support: support@composure-pro.com")}
59
+ `);
60
+ }
@@ -0,0 +1,27 @@
1
+ import { readdirSync, readFileSync, writeFileSync, unlinkSync } from "node:fs";
2
+ import { resolve, join } from "node:path";
3
+
4
+ export async function resetMigrations(projectName: string): Promise<void> {
5
+ const migrationsDir = resolve(process.cwd(), projectName, "supabase/migrations");
6
+ const files = readdirSync(migrationsDir)
7
+ .filter((f) => f.endsWith(".sql"))
8
+ .sort();
9
+
10
+ if (files.length <= 1) return; // Already a single migration
11
+
12
+ // Concatenate all migrations into one baseline
13
+ let combined = "-- Baseline migration generated by create-composure-app --clean-migrations\n\n";
14
+ for (const file of files) {
15
+ combined += `-- Source: ${file}\n`;
16
+ combined += readFileSync(join(migrationsDir, file), "utf-8");
17
+ combined += "\n\n";
18
+ }
19
+
20
+ // Delete all existing migrations
21
+ for (const file of files) {
22
+ unlinkSync(join(migrationsDir, file));
23
+ }
24
+
25
+ // Write the combined baseline
26
+ writeFileSync(join(migrationsDir, "20260101000001_init.sql"), combined, "utf-8");
27
+ }
@@ -0,0 +1,35 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { rewritePackageJsonContent } from "./rewrite-package-names.js";
3
+
4
+ describe("rewritePackageJsonContent", () => {
5
+ it("replaces @template/* references with project name", () => {
6
+ const input = JSON.stringify({
7
+ name: "@template/web",
8
+ dependencies: {
9
+ "@template/database": "workspace:*",
10
+ "@template/shared": "workspace:*",
11
+ },
12
+ });
13
+ const output = rewritePackageJsonContent(input, "my-app");
14
+ const parsed = JSON.parse(output);
15
+ expect(parsed.name).toBe("@my-app/web");
16
+ expect(parsed.dependencies["@my-app/database"]).toBe("workspace:*");
17
+ expect(parsed.dependencies["@my-app/shared"]).toBe("workspace:*");
18
+ });
19
+
20
+ it("preserves non-template dependencies", () => {
21
+ const input = JSON.stringify({
22
+ dependencies: { react: "^19.0.0", "@template/database": "workspace:*" },
23
+ });
24
+ const output = rewritePackageJsonContent(input, "acme");
25
+ const parsed = JSON.parse(output);
26
+ expect(parsed.dependencies.react).toBe("^19.0.0");
27
+ expect(parsed.dependencies["@acme/database"]).toBe("workspace:*");
28
+ });
29
+
30
+ it("handles content with no @template/ references", () => {
31
+ const input = JSON.stringify({ name: "plain-package", version: "1.0.0" });
32
+ const output = rewritePackageJsonContent(input, "my-app");
33
+ expect(output).toBe(input);
34
+ });
35
+ });