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,8 @@
1
+ export interface CreateOptions {
2
+ token?: string;
3
+ cleanMigrations?: boolean;
4
+ skipInstall?: boolean;
5
+ dev?: boolean;
6
+ tier?: string;
7
+ }
8
+ export declare function createCommand(name: string | undefined, options: CreateOptions): Promise<void>;
@@ -0,0 +1,89 @@
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 { logger } from "../lib/logger.js";
14
+ export async function createCommand(name, options) {
15
+ logger.info(kleur.bold().blue("\n create-composure-app\n"));
16
+ try {
17
+ const projectName = name ?? (await promptProjectName());
18
+ await validateTarget(projectName);
19
+ // ── License validation ──────────────────────────────────
20
+ let tier;
21
+ if (options.dev) {
22
+ logger.warn("Dev mode — skipping license validation");
23
+ // In dev mode, allow tier flag or prompt
24
+ tier = options.tier ?? (await promptInstallTier("enterprise"));
25
+ }
26
+ else {
27
+ const spinner = ora("Validating license...").start();
28
+ const license = await validateLicense(options.token);
29
+ spinner.succeed(`License validated — ${kleur.green(license.plan)} plan (${license.email})`);
30
+ // ── Interactive tier selection ──────────────────────────
31
+ logger.info("");
32
+ tier = options.tier ?? (await promptInstallTier(license.plan));
33
+ }
34
+ const includeMobile = tier === "starter-mobile" || tier === "pro-mobile";
35
+ const includePro = tier === "pro" || tier === "pro-mobile";
36
+ logger.info(`\n ${kleur.bold("Selected:")} ${tierLabel(tier)}\n`);
37
+ // ── Download ──────────────────────────────────────────────
38
+ const downloadSpinner = ora("Downloading template...").start();
39
+ await downloadTemplate(projectName, options.token, options.dev);
40
+ downloadSpinner.succeed("Template downloaded");
41
+ // ── Scaffold tier (remove unused files) ──────────────────
42
+ const scaffoldSpinner = ora("Scaffolding for selected tier...").start();
43
+ const { removed } = await scaffoldTier(projectName, tier);
44
+ if (removed.length > 0) {
45
+ scaffoldSpinner.succeed(`Scaffolded — removed ${removed.length} items not in ${tierLabel(tier)}`);
46
+ }
47
+ else {
48
+ scaffoldSpinner.succeed("Full template installed");
49
+ }
50
+ // ── Personalize ──────────────────────────────────────────
51
+ const rewriteSpinner = ora("Personalizing project...").start();
52
+ await rewritePackageNames(projectName);
53
+ rewriteSpinner.succeed("Project personalized");
54
+ // ── Migrations ───────────────────────────────────────────
55
+ if (options.cleanMigrations) {
56
+ const migSpinner = ora("Resetting migrations...").start();
57
+ await resetMigrations(projectName);
58
+ migSpinner.succeed("Migrations reset");
59
+ }
60
+ // ── Git ──────────────────────────────────────────────────
61
+ const gitSpinner = ora("Initializing git...").start();
62
+ await initGit(projectName);
63
+ gitSpinner.succeed("Git initialized");
64
+ // ── Dependencies ─────────────────────────────────────────
65
+ if (!options.skipInstall) {
66
+ const installSpinner = ora("Installing dependencies (this may take a minute)...").start();
67
+ await installDeps(projectName);
68
+ installSpinner.succeed("Dependencies installed");
69
+ }
70
+ // ── Summary ──────────────────────────────────────────────
71
+ printNextSteps(projectName, { includeMobile, includePro });
72
+ }
73
+ catch (error) {
74
+ logger.error(`\nFailed: ${error instanceof Error ? error.message : String(error)}`);
75
+ process.exit(1);
76
+ }
77
+ }
78
+ function tierLabel(tier) {
79
+ switch (tier) {
80
+ case "starter":
81
+ return "Starter (Web)";
82
+ case "starter-mobile":
83
+ return "Starter + Mobile";
84
+ case "pro":
85
+ return "Pro (Web)";
86
+ case "pro-mobile":
87
+ return "Pro + Mobile";
88
+ }
89
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from "commander";
3
+ import { createCommand } from "./commands/create.js";
4
+ const program = new Command();
5
+ program
6
+ .name("create-composure-app")
7
+ .description("Scaffold a new Composure Template project — multi-tenant Next.js + Supabase + Expo")
8
+ .version("1.0.0");
9
+ program
10
+ .argument("[name]", "Project name")
11
+ .option("-t, --token <token>", "OAuth license token (or use Composure login)")
12
+ .option("--tier <tier>", "Install tier: starter, starter-mobile, pro, pro-mobile")
13
+ .option("--clean-migrations", "Reset migrations to a single baseline", false)
14
+ .option("--skip-install", "Skip pnpm install step", false)
15
+ .option("--dev", "Development mode — skips license validation", false)
16
+ .action(createCommand);
17
+ program.parse();
@@ -0,0 +1,6 @@
1
+ export declare const logger: {
2
+ info: (msg: string) => void;
3
+ success: (msg: string) => void;
4
+ warn: (msg: string) => void;
5
+ error: (msg: string) => void;
6
+ };
@@ -0,0 +1,7 @@
1
+ import kleur from "kleur";
2
+ export const logger = {
3
+ info: (msg) => console.log(kleur.blue(msg)),
4
+ success: (msg) => console.log(kleur.green(msg)),
5
+ warn: (msg) => console.log(kleur.yellow(msg)),
6
+ error: (msg) => console.error(kleur.red(msg)),
7
+ };
@@ -0,0 +1,9 @@
1
+ import type { TemplatePlan } from "../steps/validate-license.js";
2
+ export declare function promptProjectName(): Promise<string>;
3
+ export declare function promptConfirm(message: string): Promise<boolean>;
4
+ export type InstallTier = "starter" | "starter-mobile" | "pro" | "pro-mobile";
5
+ /**
6
+ * Prompt the user to select their install tier.
7
+ * Enterprise users see all options. Free users see starter options only.
8
+ */
9
+ export declare function promptInstallTier(plan: TemplatePlan): Promise<InstallTier>;
@@ -0,0 +1,85 @@
1
+ import prompts from "prompts";
2
+ export async function promptProjectName() {
3
+ const { name } = await prompts({
4
+ type: "text",
5
+ name: "name",
6
+ message: "Project name",
7
+ validate: (value) => /^[a-z0-9-_]+$/i.test(value) || "Use letters, numbers, dashes, and underscores only",
8
+ });
9
+ if (!name) {
10
+ throw new Error("Project name is required.");
11
+ }
12
+ return name;
13
+ }
14
+ export async function promptConfirm(message) {
15
+ const { confirmed } = await prompts({
16
+ type: "confirm",
17
+ name: "confirmed",
18
+ message,
19
+ initial: true,
20
+ });
21
+ return confirmed ?? false;
22
+ }
23
+ const ALL_TIERS = [
24
+ {
25
+ title: "Starter (Web only)",
26
+ value: "starter",
27
+ description: "Auth, accounts, users, contacts, settings, team",
28
+ },
29
+ {
30
+ title: "Starter + Mobile",
31
+ value: "starter-mobile",
32
+ description: "Starter + Expo app with 3 tenant tab layouts",
33
+ },
34
+ {
35
+ title: "Pro (Web only)",
36
+ value: "pro",
37
+ description: "All 30 tables, all screens, CRM, inbox, workflows, AI, commerce",
38
+ },
39
+ {
40
+ title: "Pro + Mobile",
41
+ value: "pro-mobile",
42
+ description: "Everything + Expo mobile app",
43
+ },
44
+ ];
45
+ /**
46
+ * Get available tiers based on license plan.
47
+ *
48
+ * - enterprise: all tiers
49
+ * - pro: all tiers
50
+ * - free: starter tiers only
51
+ */
52
+ function getAvailableTiers(plan) {
53
+ switch (plan) {
54
+ case "enterprise":
55
+ case "pro":
56
+ return ALL_TIERS;
57
+ case "free":
58
+ default:
59
+ return ALL_TIERS.filter((t) => t.value === "starter" || t.value === "starter-mobile");
60
+ }
61
+ }
62
+ /**
63
+ * Prompt the user to select their install tier.
64
+ * Enterprise users see all options. Free users see starter options only.
65
+ */
66
+ export async function promptInstallTier(plan) {
67
+ const available = getAvailableTiers(plan);
68
+ // If enterprise with only one sensible choice, still show the prompt
69
+ // so internal users can choose web-only vs web+mobile
70
+ const { tier } = await prompts({
71
+ type: "select",
72
+ name: "tier",
73
+ message: "What would you like to install?",
74
+ choices: available.map((t) => ({
75
+ title: t.title,
76
+ value: t.value,
77
+ description: t.description,
78
+ })),
79
+ initial: available.length - 1, // Default to the highest tier available
80
+ });
81
+ if (!tier) {
82
+ throw new Error("Installation cancelled.");
83
+ }
84
+ return tier;
85
+ }
@@ -0,0 +1 @@
1
+ export declare function downloadTemplate(projectName: string, token?: string, devMode?: boolean): Promise<void>;
@@ -0,0 +1,38 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { resolve, basename } from "node:path";
3
+ const TEMPLATE_REPO = process.env.TEMPLATE_REPO_URL ?? "https://github.com/hrconsultnj/template-monorepo.git";
4
+ export async function downloadTemplate(projectName, token, devMode) {
5
+ // Defense-in-depth: ensure projectName is just a directory name, no path traversal
6
+ if (basename(projectName) !== projectName) {
7
+ throw new Error(`Invalid project name: "${projectName}" contains path separators.`);
8
+ }
9
+ const targetPath = resolve(process.cwd(), projectName);
10
+ if (devMode) {
11
+ // execFileSync bypasses the shell — no injection possible
12
+ execFileSync("git", [
13
+ "clone", "--depth=1", "--single-branch", TEMPLATE_REPO, targetPath,
14
+ ], { stdio: "pipe" });
15
+ return;
16
+ }
17
+ // Production mode: download tarball from license server
18
+ const LICENSE_SERVER = process.env.TEMPLATE_LICENSE_SERVER ?? "https://composure-pro.com";
19
+ const response = await fetch(`${LICENSE_SERVER}/download`, {
20
+ headers: {
21
+ Authorization: `Bearer ${token}`,
22
+ },
23
+ });
24
+ if (!response.ok) {
25
+ throw new Error(`Download failed: ${response.statusText}`);
26
+ }
27
+ const { mkdirSync, writeFileSync } = await import("node:fs");
28
+ mkdirSync(targetPath, { recursive: true });
29
+ const buffer = Buffer.from(await response.arrayBuffer());
30
+ const tarballPath = resolve(targetPath, ".template.tar.gz");
31
+ writeFileSync(tarballPath, buffer);
32
+ // --no-absolute-names prevents path traversal in tarball entries
33
+ execFileSync("tar", [
34
+ "xzf", ".template.tar.gz", "--strip-components=1", "--no-absolute-names",
35
+ ], { cwd: targetPath, stdio: "pipe" });
36
+ const { unlinkSync } = await import("node:fs");
37
+ unlinkSync(tarballPath);
38
+ }
@@ -0,0 +1 @@
1
+ export declare function initGit(projectName: string): Promise<void>;
@@ -0,0 +1,12 @@
1
+ import { execSync } from "node:child_process";
2
+ import { rmSync } from "node:fs";
3
+ import { resolve } from "node:path";
4
+ export async function initGit(projectName) {
5
+ const targetPath = resolve(process.cwd(), projectName);
6
+ // Remove the cloned .git directory
7
+ rmSync(resolve(targetPath, ".git"), { recursive: true, force: true });
8
+ // Initialize fresh git repo
9
+ execSync("git init", { cwd: targetPath, stdio: "pipe" });
10
+ execSync("git add -A", { cwd: targetPath, stdio: "pipe" });
11
+ execSync('git commit -m "chore: initial commit from create-composure-app"', { cwd: targetPath, stdio: "pipe" });
12
+ }
@@ -0,0 +1 @@
1
+ export declare function installDeps(projectName: string): Promise<void>;
@@ -0,0 +1,6 @@
1
+ import { execSync } from "node:child_process";
2
+ import { resolve } from "node:path";
3
+ export async function installDeps(projectName) {
4
+ const targetPath = resolve(process.cwd(), projectName);
5
+ execSync("pnpm install", { cwd: targetPath, stdio: "pipe" });
6
+ }
@@ -0,0 +1,6 @@
1
+ interface NextStepsOptions {
2
+ includeMobile?: boolean;
3
+ includePro?: boolean;
4
+ }
5
+ export declare function printNextSteps(projectName: string, options?: NextStepsOptions): void;
6
+ export {};
@@ -0,0 +1,39 @@
1
+ import kleur from "kleur";
2
+ export function printNextSteps(projectName, options = {}) {
3
+ const { includeMobile = false, includePro = false } = options;
4
+ const name = kleur.cyan(projectName);
5
+ const features = [
6
+ `- ${kleur.blue("Web app")} — Next.js 16 + Tailwind + shadcn`,
7
+ `- ${kleur.blue("Database")} — Supabase + typed queries + RLS`,
8
+ `- ${kleur.blue("3 tenant types")} — Admin / Workspace / Portal`,
9
+ `- ${kleur.blue("Auth")} — Contact-first signup + multi-account`,
10
+ ];
11
+ if (includeMobile) {
12
+ features.push(`- ${kleur.blue("Mobile app")} — Expo SDK 55 + React Native`);
13
+ }
14
+ if (includePro) {
15
+ features.push(`- ${kleur.blue("CRM")} — Tasks, deals, pipelines`, `- ${kleur.blue("Inbox")} — Threads, compose, reply`, `- ${kleur.blue("AI")} — Agents, memory graph, thinking`, `- ${kleur.blue("Commerce")} — Invoices, quotes, payments`, `- ${kleur.blue("Workflows")} — Visual automation engine`);
16
+ }
17
+ console.log(`
18
+ ${kleur.green().bold("Your project is ready!")}
19
+
20
+ ${kleur.bold("Next steps:")}
21
+ ${kleur.gray("$")} cd ${name}
22
+ ${kleur.gray("$")} pnpm supabase start
23
+ ${kleur.gray("$")} pnpm dev
24
+
25
+ ${kleur.bold("First-time setup:")}
26
+ 1. Copy ${kleur.yellow(".env.example")} to ${kleur.yellow(".env")}
27
+ 2. Fill in your Supabase keys
28
+ 3. Run ${kleur.yellow(`pnpm --filter @${projectName}/database db:types`)} after migrations apply
29
+ 4. Visit ${kleur.underline("http://localhost:3000")} to sign up
30
+
31
+ ${kleur.bold("What's included:")}
32
+ ${features.map((f) => ` ${f}`).join("\n")}
33
+ ${!includePro ? `\n${kleur.yellow("Upgrade to Pro for CRM, inbox, workflows, AI, and commerce.")}` : ""}
34
+ ${!includeMobile ? `\n${kleur.yellow("Add mobile with the + Mobile tier for Expo SDK 55 support.")}` : ""}
35
+
36
+ ${kleur.gray("Docs: https://composure-pro.com/template/docs")}
37
+ ${kleur.gray("Support: support@composure-pro.com")}
38
+ `);
39
+ }
@@ -0,0 +1 @@
1
+ export declare function resetMigrations(projectName: string): Promise<void>;
@@ -0,0 +1,23 @@
1
+ import { readdirSync, readFileSync, writeFileSync, unlinkSync } from "node:fs";
2
+ import { resolve, join } from "node:path";
3
+ export async function resetMigrations(projectName) {
4
+ const migrationsDir = resolve(process.cwd(), projectName, "supabase/migrations");
5
+ const files = readdirSync(migrationsDir)
6
+ .filter((f) => f.endsWith(".sql"))
7
+ .sort();
8
+ if (files.length <= 1)
9
+ return; // Already a single migration
10
+ // Concatenate all migrations into one baseline
11
+ let combined = "-- Baseline migration generated by create-composure-app --clean-migrations\n\n";
12
+ for (const file of files) {
13
+ combined += `-- Source: ${file}\n`;
14
+ combined += readFileSync(join(migrationsDir, file), "utf-8");
15
+ combined += "\n\n";
16
+ }
17
+ // Delete all existing migrations
18
+ for (const file of files) {
19
+ unlinkSync(join(migrationsDir, file));
20
+ }
21
+ // Write the combined baseline
22
+ writeFileSync(join(migrationsDir, "20260101000001_init.sql"), combined, "utf-8");
23
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Pure function: rewrite @template/* references in a package.json string.
3
+ * Exported separately for unit testing.
4
+ */
5
+ export declare function rewritePackageJsonContent(content: string, projectName: string): string;
6
+ /**
7
+ * Rewrite all @template/* references to @<projectName>/* across the project.
8
+ */
9
+ export declare function rewritePackageNames(projectName: string): Promise<void>;
@@ -0,0 +1,51 @@
1
+ import { readFileSync, writeFileSync } from "node:fs";
2
+ import { resolve, join } from "node:path";
3
+ import { readdirSync, statSync } from "node:fs";
4
+ /**
5
+ * Pure function: rewrite @template/* references in a package.json string.
6
+ * Exported separately for unit testing.
7
+ */
8
+ export function rewritePackageJsonContent(content, projectName) {
9
+ return content.replace(/@template\//g, `@${projectName}/`);
10
+ }
11
+ /**
12
+ * Walk a directory tree and find all package.json files.
13
+ */
14
+ function findPackageJsonFiles(dir) {
15
+ const results = [];
16
+ for (const entry of readdirSync(dir)) {
17
+ if (entry === "node_modules" || entry === ".git")
18
+ continue;
19
+ const fullPath = join(dir, entry);
20
+ const stat = statSync(fullPath);
21
+ if (stat.isDirectory()) {
22
+ results.push(...findPackageJsonFiles(fullPath));
23
+ }
24
+ else if (entry === "package.json") {
25
+ results.push(fullPath);
26
+ }
27
+ }
28
+ return results;
29
+ }
30
+ /**
31
+ * Rewrite all @template/* references to @<projectName>/* across the project.
32
+ */
33
+ export async function rewritePackageNames(projectName) {
34
+ const targetPath = resolve(process.cwd(), projectName);
35
+ const packageFiles = findPackageJsonFiles(targetPath);
36
+ for (const file of packageFiles) {
37
+ const content = readFileSync(file, "utf-8");
38
+ if (content.includes("@template/")) {
39
+ const rewritten = rewritePackageJsonContent(content, projectName);
40
+ writeFileSync(file, rewritten, "utf-8");
41
+ }
42
+ }
43
+ // Also rewrite the root package.json name
44
+ const rootPkg = resolve(targetPath, "package.json");
45
+ const rootContent = readFileSync(rootPkg, "utf-8");
46
+ const parsed = JSON.parse(rootContent);
47
+ if (parsed.name === "template-monorepo") {
48
+ parsed.name = projectName;
49
+ writeFileSync(rootPkg, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
50
+ }
51
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,32 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { rewritePackageJsonContent } from "./rewrite-package-names.js";
3
+ describe("rewritePackageJsonContent", () => {
4
+ it("replaces @template/* references with project name", () => {
5
+ const input = JSON.stringify({
6
+ name: "@template/web",
7
+ dependencies: {
8
+ "@template/database": "workspace:*",
9
+ "@template/shared": "workspace:*",
10
+ },
11
+ });
12
+ const output = rewritePackageJsonContent(input, "my-app");
13
+ const parsed = JSON.parse(output);
14
+ expect(parsed.name).toBe("@my-app/web");
15
+ expect(parsed.dependencies["@my-app/database"]).toBe("workspace:*");
16
+ expect(parsed.dependencies["@my-app/shared"]).toBe("workspace:*");
17
+ });
18
+ it("preserves non-template dependencies", () => {
19
+ const input = JSON.stringify({
20
+ dependencies: { react: "^19.0.0", "@template/database": "workspace:*" },
21
+ });
22
+ const output = rewritePackageJsonContent(input, "acme");
23
+ const parsed = JSON.parse(output);
24
+ expect(parsed.dependencies.react).toBe("^19.0.0");
25
+ expect(parsed.dependencies["@acme/database"]).toBe("workspace:*");
26
+ });
27
+ it("handles content with no @template/ references", () => {
28
+ const input = JSON.stringify({ name: "plain-package", version: "1.0.0" });
29
+ const output = rewritePackageJsonContent(input, "my-app");
30
+ expect(output).toBe(input);
31
+ });
32
+ });
@@ -0,0 +1,13 @@
1
+ import type { InstallTier } from "../lib/prompts.js";
2
+ /**
3
+ * Remove files and folders that don't belong in the selected tier.
4
+ *
5
+ * Tier hierarchy:
6
+ * starter → core web only
7
+ * starter-mobile → core web + mobile
8
+ * pro → all web features
9
+ * pro-mobile → all web features + mobile
10
+ */
11
+ export declare function scaffoldTier(projectName: string, tier: InstallTier): Promise<{
12
+ removed: string[];
13
+ }>;
@@ -0,0 +1,141 @@
1
+ import { resolve } from "node:path";
2
+ import { rmSync, existsSync, readFileSync, writeFileSync } from "node:fs";
3
+ /**
4
+ * Remove files and folders that don't belong in the selected tier.
5
+ *
6
+ * Tier hierarchy:
7
+ * starter → core web only
8
+ * starter-mobile → core web + mobile
9
+ * pro → all web features
10
+ * pro-mobile → all web features + mobile
11
+ */
12
+ export async function scaffoldTier(projectName, tier) {
13
+ const root = resolve(process.cwd(), projectName);
14
+ const removed = [];
15
+ const includeMobile = tier === "starter-mobile" || tier === "pro-mobile";
16
+ const includePro = tier === "pro" || tier === "pro-mobile";
17
+ // ── Remove mobile app if not included ───────────────────────
18
+ if (!includeMobile) {
19
+ const mobilePath = resolve(root, "apps/mobile");
20
+ if (existsSync(mobilePath)) {
21
+ rmSync(mobilePath, { recursive: true, force: true });
22
+ removed.push("apps/mobile");
23
+ }
24
+ // Remove mobile from pnpm-workspace.yaml
25
+ updateWorkspaceYaml(root, false);
26
+ }
27
+ // ── Remove pro features if starter tier ─────────────────────
28
+ if (!includePro) {
29
+ // Pro-only route pages (workspace)
30
+ const proWorkspaceRoutes = [
31
+ "inbox",
32
+ "events",
33
+ "timecards",
34
+ "forms",
35
+ "templates",
36
+ "commercial",
37
+ "connected-services",
38
+ "documents",
39
+ "tags",
40
+ ];
41
+ for (const route of proWorkspaceRoutes) {
42
+ const routePath = resolve(root, `apps/web/app/(protected)/(tenant)/workspace/[id_prefix]/${route}`);
43
+ if (existsSync(routePath)) {
44
+ rmSync(routePath, { recursive: true, force: true });
45
+ removed.push(`workspace/${route}`);
46
+ }
47
+ }
48
+ // Pro-only route pages (admin)
49
+ const proAdminRoutes = [
50
+ "broadcasts",
51
+ "ai-agents",
52
+ "integrations",
53
+ ];
54
+ for (const route of proAdminRoutes) {
55
+ const routePath = resolve(root, `apps/web/app/(protected)/(internal)/admin/[id_prefix]/${route}`);
56
+ if (existsSync(routePath)) {
57
+ rmSync(routePath, { recursive: true, force: true });
58
+ removed.push(`admin/${route}`);
59
+ }
60
+ }
61
+ // Pro-only route pages (portal)
62
+ const proPortalRoutes = [
63
+ "documents",
64
+ "messages",
65
+ "invoices",
66
+ ];
67
+ for (const route of proPortalRoutes) {
68
+ const routePath = resolve(root, `apps/web/app/(protected)/(external)/portal/[id_prefix]/${route}`);
69
+ if (existsSync(routePath)) {
70
+ rmSync(routePath, { recursive: true, force: true });
71
+ removed.push(`portal/${route}`);
72
+ }
73
+ }
74
+ // Pro-only components
75
+ const proComponents = [
76
+ "inbox",
77
+ "events",
78
+ "timecards",
79
+ "forms",
80
+ "templates",
81
+ "commercial",
82
+ "connected-services",
83
+ "documents",
84
+ "tags",
85
+ ];
86
+ for (const component of proComponents) {
87
+ const componentPath = resolve(root, `apps/web/components/${component}`);
88
+ if (existsSync(componentPath)) {
89
+ rmSync(componentPath, { recursive: true, force: true });
90
+ removed.push(`components/${component}`);
91
+ }
92
+ }
93
+ // Pro-only migrations
94
+ const proMigrations = [
95
+ "20260101000012_docs.sql",
96
+ "20260101000013_inboxes.sql",
97
+ "20260101000014_inbox_threads.sql",
98
+ "20260101000015_inbox_messages.sql",
99
+ "20260101000016_events.sql",
100
+ "20260101000017_timecards.sql",
101
+ "20260101000020_docs_versions.sql",
102
+ "20260127000001_broadcasts.sql",
103
+ "20260127000002_ai_system.sql",
104
+ "20260127000003_inbox_seed.sql",
105
+ "20260128000001_document_payment_enums.sql",
106
+ "20260128000002_tags.sql",
107
+ "20260128000003_entity_documents.sql",
108
+ "20260128000004_entity_document_storage.sql",
109
+ "20260128000005_commercial_documents_enums.sql",
110
+ "20260128000006_commercial_documents.sql",
111
+ "20260128000007_payments.sql",
112
+ "20260403000001_ai_thinking_system.sql",
113
+ "20260409000001_connected_services.sql",
114
+ ];
115
+ for (const migration of proMigrations) {
116
+ const migrationPath = resolve(root, `supabase/migrations/${migration}`);
117
+ if (existsSync(migrationPath)) {
118
+ rmSync(migrationPath, { force: true });
119
+ removed.push(`migrations/${migration}`);
120
+ }
121
+ }
122
+ }
123
+ return { removed };
124
+ }
125
+ /**
126
+ * Update pnpm-workspace.yaml to include/exclude mobile
127
+ */
128
+ function updateWorkspaceYaml(root, includeMobile) {
129
+ const workspacePath = resolve(root, "pnpm-workspace.yaml");
130
+ if (!existsSync(workspacePath))
131
+ return;
132
+ let content = readFileSync(workspacePath, "utf8");
133
+ if (!includeMobile) {
134
+ // Remove apps/mobile line
135
+ content = content
136
+ .split("\n")
137
+ .filter((line) => !line.includes("apps/mobile"))
138
+ .join("\n");
139
+ writeFileSync(workspacePath, content, "utf8");
140
+ }
141
+ }
@@ -0,0 +1,17 @@
1
+ export type TemplatePlan = "free" | "pro" | "enterprise";
2
+ export interface LicenseInfo {
3
+ valid: boolean;
4
+ plan: TemplatePlan;
5
+ email: string;
6
+ features: string[];
7
+ }
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 declare function validateLicense(token: string | undefined): Promise<LicenseInfo>;