create-pilotprojects-app 0.1.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.
- package/dist/helpers/copy.js +32 -0
- package/dist/helpers/install.js +18 -0
- package/dist/helpers/package-json.js +25 -0
- package/dist/index.js +54 -0
- package/dist/prompts.js +75 -0
- package/dist/scaffold.js +84 -0
- package/dist/types.js +1 -0
- package/package.json +39 -0
- package/templates/apps/mobile/.env.example +15 -0
- package/templates/apps/mobile/app/(auth)/login.tsx +86 -0
- package/templates/apps/mobile/app/(tabs)/_layout.tsx +10 -0
- package/templates/apps/mobile/app/(tabs)/index.tsx +30 -0
- package/templates/apps/mobile/app/_layout.tsx +58 -0
- package/templates/apps/mobile/app.config.ts +73 -0
- package/templates/apps/mobile/babel.config.js +9 -0
- package/templates/apps/mobile/eas.json +48 -0
- package/templates/apps/mobile/global.css +44 -0
- package/templates/apps/mobile/lib/supabase.ts +15 -0
- package/templates/apps/mobile/lib/trpc/client.ts +30 -0
- package/templates/apps/mobile/package.json +48 -0
- package/templates/apps/mobile/tsconfig.json +16 -0
- package/templates/apps/web/.env.development.example +22 -0
- package/templates/apps/web/app/(auth)/login/page.tsx +102 -0
- package/templates/apps/web/app/(dashboard)/dashboard/page.tsx +21 -0
- package/templates/apps/web/app/(dashboard)/layout.tsx +14 -0
- package/templates/apps/web/app/api/trpc/[trpc]/route.ts +32 -0
- package/templates/apps/web/app/globals.css +51 -0
- package/templates/apps/web/app/layout.tsx +25 -0
- package/templates/apps/web/lib/trpc/client.tsx +7 -0
- package/templates/apps/web/lib/trpc/provider.tsx +37 -0
- package/templates/apps/web/lib/trpc/server.ts +20 -0
- package/templates/apps/web/middleware.ts +10 -0
- package/templates/apps/web/package.json +44 -0
- package/templates/apps/web/postcss.config.js +6 -0
- package/templates/apps/web/sentry.client.config.ts +10 -0
- package/templates/apps/web/sentry.edge.config.ts +6 -0
- package/templates/apps/web/sentry.server.config.ts +7 -0
- package/templates/apps/web/tailwind.config.ts +12 -0
- package/templates/apps/web/tsconfig.json +16 -0
- package/templates/apps/web/types/css.d.ts +1 -0
- package/templates/base/.prettierrc.js +9 -0
- package/templates/base/_gitignore +30 -0
- package/templates/base/package.json +30 -0
- package/templates/base/turbo.json +33 -0
- package/templates/packages/api/package.json +23 -0
- package/templates/packages/api/src/features/auth/auth.router.ts +8 -0
- package/templates/packages/api/src/features/auth/auth.schema.ts +9 -0
- package/templates/packages/api/src/features/auth/auth.service.ts +24 -0
- package/templates/packages/api/src/features/user/user.router.ts +21 -0
- package/templates/packages/api/src/features/user/user.schema.ts +13 -0
- package/templates/packages/api/src/features/user/user.service.ts +41 -0
- package/templates/packages/api/src/index.ts +9 -0
- package/templates/packages/api/src/root.ts +10 -0
- package/templates/packages/api/src/trpc.ts +42 -0
- package/templates/packages/api/tsconfig.json +4 -0
- package/templates/packages/auth/package.json +26 -0
- package/templates/packages/auth/src/client.ts +8 -0
- package/templates/packages/auth/src/index.ts +3 -0
- package/templates/packages/auth/src/middleware.ts +25 -0
- package/templates/packages/auth/src/server.ts +15 -0
- package/templates/packages/auth/tsconfig.json +4 -0
- package/templates/packages/config/eslint/base.js +28 -0
- package/templates/packages/config/eslint/nextjs.js +10 -0
- package/templates/packages/config/eslint/react-native.js +18 -0
- package/templates/packages/config/package.json +13 -0
- package/templates/packages/config/typescript/base.json +17 -0
- package/templates/packages/config/typescript/nextjs.json +18 -0
- package/templates/packages/config/typescript/react-native.json +11 -0
- package/templates/packages/db/.env.example +1 -0
- package/templates/packages/db/drizzle.config.ts +13 -0
- package/templates/packages/db/package.json +28 -0
- package/templates/packages/db/src/index.ts +14 -0
- package/templates/packages/db/src/schema/index.ts +1 -0
- package/templates/packages/db/src/schema/users.ts +13 -0
- package/templates/packages/db/tsconfig.json +4 -0
- package/templates/packages/email/package.json +23 -0
- package/templates/packages/email/src/index.ts +2 -0
- package/templates/packages/email/src/resend.ts +8 -0
- package/templates/packages/email/src/templates/welcome.tsx +54 -0
- package/templates/packages/email/tsconfig.json +8 -0
- package/templates/packages/ui/package.json +38 -0
- package/templates/packages/ui/src/lib/utils.ts +6 -0
- package/templates/packages/ui/src/native/button.tsx +58 -0
- package/templates/packages/ui/src/native/card.tsx +41 -0
- package/templates/packages/ui/src/native/input.tsx +24 -0
- package/templates/packages/ui/src/native.d.ts +24 -0
- package/templates/packages/ui/src/tailwind-preset.ts +50 -0
- package/templates/packages/ui/src/web/button.tsx +49 -0
- package/templates/packages/ui/src/web/card.tsx +51 -0
- package/templates/packages/ui/src/web/input.tsx +22 -0
- package/templates/packages/ui/tsconfig.json +8 -0
- package/templates/packages/validators/package.json +20 -0
- package/templates/packages/validators/src/auth.ts +15 -0
- package/templates/packages/validators/src/index.ts +2 -0
- package/templates/packages/validators/src/user.ts +8 -0
- package/templates/packages/validators/tsconfig.json +4 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import fs from "fs-extra";
|
|
2
|
+
import path from "path";
|
|
3
|
+
function interpolate(content, vars) {
|
|
4
|
+
return content
|
|
5
|
+
.replaceAll("{{PROJECT_NAME}}", vars.PROJECT_NAME)
|
|
6
|
+
.replaceAll("{{PACKAGE_SCOPE}}", vars.PACKAGE_SCOPE)
|
|
7
|
+
.replaceAll("{{PACKAGE_SCOPE_SAFE}}", vars.PACKAGE_SCOPE_SAFE);
|
|
8
|
+
}
|
|
9
|
+
export async function copyTemplate(templateDir, destDir, vars) {
|
|
10
|
+
await fs.ensureDir(destDir);
|
|
11
|
+
const entries = await fs.readdir(templateDir, { withFileTypes: true });
|
|
12
|
+
for (const entry of entries) {
|
|
13
|
+
const srcPath = path.join(templateDir, entry.name);
|
|
14
|
+
// Rename _gitignore → .gitignore (npm strips .gitignore from published packages)
|
|
15
|
+
const destName = entry.name === "_gitignore" ? ".gitignore" : entry.name;
|
|
16
|
+
const destPath = path.join(destDir, destName);
|
|
17
|
+
if (entry.isDirectory()) {
|
|
18
|
+
await copyTemplate(srcPath, destPath, vars);
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
const raw = await fs.readFile(srcPath, "utf-8");
|
|
22
|
+
await fs.outputFile(destPath, interpolate(raw, vars), "utf-8");
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export function makeVars(projectName, packageScope) {
|
|
27
|
+
return {
|
|
28
|
+
PROJECT_NAME: projectName,
|
|
29
|
+
PACKAGE_SCOPE: packageScope,
|
|
30
|
+
PACKAGE_SCOPE_SAFE: packageScope.replace("@", "").replace("/", "-"),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { execa } from "execa";
|
|
2
|
+
const INSTALL_CMD = {
|
|
3
|
+
pnpm: ["pnpm", ["install"]],
|
|
4
|
+
npm: ["npm", ["install"]],
|
|
5
|
+
yarn: ["yarn", ["install", "--frozen-lockfile"]],
|
|
6
|
+
};
|
|
7
|
+
export async function installDeps(cwd, pm) {
|
|
8
|
+
const [cmd, args] = [
|
|
9
|
+
pm,
|
|
10
|
+
pm === "pnpm" ? ["install"] : pm === "yarn" ? ["install"] : ["install"],
|
|
11
|
+
];
|
|
12
|
+
await execa(cmd, args, { cwd, stdio: "inherit" });
|
|
13
|
+
}
|
|
14
|
+
export async function gitInit(cwd) {
|
|
15
|
+
await execa("git", ["init"], { cwd });
|
|
16
|
+
await execa("git", ["add", "-A"], { cwd });
|
|
17
|
+
await execa("git", ["commit", "-m", "chore: initial scaffold"], { cwd });
|
|
18
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import fs from "fs-extra";
|
|
2
|
+
import path from "path";
|
|
3
|
+
export async function readPkg(dir) {
|
|
4
|
+
return fs.readJson(path.join(dir, "package.json"));
|
|
5
|
+
}
|
|
6
|
+
export async function writePkg(dir, pkg) {
|
|
7
|
+
await fs.writeJson(path.join(dir, "package.json"), pkg, { spaces: 2 });
|
|
8
|
+
}
|
|
9
|
+
export async function mergePkg(dir, patch) {
|
|
10
|
+
const existing = await readPkg(dir);
|
|
11
|
+
await writePkg(dir, { ...existing, ...patch });
|
|
12
|
+
}
|
|
13
|
+
/** Remove a list of dependency keys from dependencies + devDependencies */
|
|
14
|
+
export async function removeDeps(dir, keys) {
|
|
15
|
+
const pkg = await readPkg(dir);
|
|
16
|
+
const deps = (pkg.dependencies ?? {});
|
|
17
|
+
const devDeps = (pkg.devDependencies ?? {});
|
|
18
|
+
for (const key of keys) {
|
|
19
|
+
delete deps[key];
|
|
20
|
+
delete devDeps[key];
|
|
21
|
+
}
|
|
22
|
+
pkg.dependencies = deps;
|
|
23
|
+
pkg.devDependencies = devDeps;
|
|
24
|
+
await writePkg(dir, pkg);
|
|
25
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import path from "path";
|
|
3
|
+
import fs from "fs-extra";
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import * as p from "@clack/prompts";
|
|
6
|
+
import pc from "picocolors";
|
|
7
|
+
import { runPrompts } from "./prompts.js";
|
|
8
|
+
import { scaffold } from "./scaffold.js";
|
|
9
|
+
const program = new Command();
|
|
10
|
+
program
|
|
11
|
+
.name("create-pilotprojects-app")
|
|
12
|
+
.description("Scaffold a Pilotprojects monorepo boilerplate")
|
|
13
|
+
.version("0.1.0")
|
|
14
|
+
.argument("[project-name]", "Name of the project to create")
|
|
15
|
+
.action(async (projectNameArg) => {
|
|
16
|
+
console.log();
|
|
17
|
+
const config = await runPrompts(projectNameArg);
|
|
18
|
+
const destDir = path.resolve(process.cwd(), config.projectName);
|
|
19
|
+
if (await fs.pathExists(destDir)) {
|
|
20
|
+
const overwrite = await p.confirm({
|
|
21
|
+
message: `Directory ${pc.yellow(config.projectName)} already exists. Overwrite?`,
|
|
22
|
+
initialValue: false,
|
|
23
|
+
});
|
|
24
|
+
if (p.isCancel(overwrite) || !overwrite) {
|
|
25
|
+
p.cancel("Scaffolding cancelled.");
|
|
26
|
+
process.exit(0);
|
|
27
|
+
}
|
|
28
|
+
await fs.remove(destDir);
|
|
29
|
+
}
|
|
30
|
+
await fs.ensureDir(destDir);
|
|
31
|
+
try {
|
|
32
|
+
await scaffold(config, destDir);
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
p.cancel(`Scaffolding failed: ${err.message}`);
|
|
36
|
+
await fs.remove(destDir);
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
p.outro([
|
|
40
|
+
pc.green("✔ Project created successfully!\n"),
|
|
41
|
+
pc.bold("Next steps:"),
|
|
42
|
+
` cd ${pc.cyan(config.projectName)}`,
|
|
43
|
+
` cp apps/web/.env.development.example apps/web/.env.development`,
|
|
44
|
+
config.apps.includes("mobile")
|
|
45
|
+
? ` cp apps/mobile/.env.example apps/mobile/.env`
|
|
46
|
+
: "",
|
|
47
|
+
` ${config.packageManager} run dev`,
|
|
48
|
+
"",
|
|
49
|
+
pc.dim("See docs/ for full setup instructions."),
|
|
50
|
+
]
|
|
51
|
+
.filter(Boolean)
|
|
52
|
+
.join("\n"));
|
|
53
|
+
});
|
|
54
|
+
program.parse();
|
package/dist/prompts.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import * as p from "@clack/prompts";
|
|
2
|
+
import pc from "picocolors";
|
|
3
|
+
export async function runPrompts(projectNameArg) {
|
|
4
|
+
p.intro(pc.bgCyan(pc.black(" create-pilotprojects-app ")));
|
|
5
|
+
const answers = await p.group({
|
|
6
|
+
projectName: () => p.text({
|
|
7
|
+
message: "What is your project name?",
|
|
8
|
+
placeholder: "my-app",
|
|
9
|
+
initialValue: projectNameArg ?? "",
|
|
10
|
+
validate: (v) => {
|
|
11
|
+
if (!v)
|
|
12
|
+
return "Project name is required";
|
|
13
|
+
if (!/^[a-z0-9-]+$/.test(v))
|
|
14
|
+
return "Use lowercase letters, numbers, and hyphens only";
|
|
15
|
+
},
|
|
16
|
+
}),
|
|
17
|
+
packageScope: ({ results }) => p.text({
|
|
18
|
+
message: "Package scope for internal packages?",
|
|
19
|
+
placeholder: `@${results.projectName}`,
|
|
20
|
+
initialValue: `@${results.projectName}`,
|
|
21
|
+
validate: (v) => {
|
|
22
|
+
if (!v)
|
|
23
|
+
return "Scope is required";
|
|
24
|
+
if (!v.startsWith("@"))
|
|
25
|
+
return "Scope must start with @";
|
|
26
|
+
},
|
|
27
|
+
}),
|
|
28
|
+
apps: () => p.multiselect({
|
|
29
|
+
message: "Which apps do you want to scaffold?",
|
|
30
|
+
options: [
|
|
31
|
+
{ value: "web", label: "Web", hint: "Next.js 15 + tRPC + Drizzle" },
|
|
32
|
+
{ value: "mobile", label: "Mobile", hint: "Expo SDK 52 + React Native" },
|
|
33
|
+
],
|
|
34
|
+
initialValues: ["web", "mobile"],
|
|
35
|
+
required: true,
|
|
36
|
+
}),
|
|
37
|
+
designSystem: () => p.confirm({
|
|
38
|
+
message: "Include the default design system? (Tailwind + ShadCN web / NativeWind mobile)",
|
|
39
|
+
initialValue: true,
|
|
40
|
+
}),
|
|
41
|
+
sentry: () => p.confirm({
|
|
42
|
+
message: "Include Sentry for error monitoring?",
|
|
43
|
+
initialValue: true,
|
|
44
|
+
}),
|
|
45
|
+
resend: () => p.confirm({
|
|
46
|
+
message: "Include Resend for transactional email? (@repo/email + React Email templates)",
|
|
47
|
+
initialValue: true,
|
|
48
|
+
}),
|
|
49
|
+
posthog: () => p.confirm({
|
|
50
|
+
message: "Include PostHog for product analytics + feature flags?",
|
|
51
|
+
initialValue: false,
|
|
52
|
+
}),
|
|
53
|
+
googleAnalytics: ({ results }) => results.apps?.includes("web")
|
|
54
|
+
? p.confirm({
|
|
55
|
+
message: "Include Google Analytics (GA4) on the web app?",
|
|
56
|
+
initialValue: false,
|
|
57
|
+
})
|
|
58
|
+
: Promise.resolve(false),
|
|
59
|
+
packageManager: () => p.select({
|
|
60
|
+
message: "Which package manager?",
|
|
61
|
+
options: [
|
|
62
|
+
{ value: "pnpm", label: "pnpm", hint: "recommended" },
|
|
63
|
+
{ value: "npm", label: "npm" },
|
|
64
|
+
{ value: "yarn", label: "yarn" },
|
|
65
|
+
],
|
|
66
|
+
initialValue: "pnpm",
|
|
67
|
+
}),
|
|
68
|
+
}, {
|
|
69
|
+
onCancel: () => {
|
|
70
|
+
p.cancel("Scaffolding cancelled.");
|
|
71
|
+
process.exit(0);
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
return answers;
|
|
75
|
+
}
|
package/dist/scaffold.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import { fileURLToPath } from "url";
|
|
3
|
+
import fs from "fs-extra";
|
|
4
|
+
import * as p from "@clack/prompts";
|
|
5
|
+
import { copyTemplate, makeVars } from "./helpers/copy.js";
|
|
6
|
+
import { removeDeps } from "./helpers/package-json.js";
|
|
7
|
+
import { installDeps, gitInit } from "./helpers/install.js";
|
|
8
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const TEMPLATES_DIR = path.resolve(__dirname, "../templates");
|
|
10
|
+
export async function scaffold(config, destDir) {
|
|
11
|
+
const vars = makeVars(config.projectName, config.packageScope);
|
|
12
|
+
const spin = p.spinner();
|
|
13
|
+
// ── 1. Base files (turbo.json, pnpm-workspace.yaml, root package.json, .gitignore) ──
|
|
14
|
+
spin.start("Copying base files…");
|
|
15
|
+
await copyTemplate(path.join(TEMPLATES_DIR, "base"), destDir, vars);
|
|
16
|
+
spin.stop("Base files copied");
|
|
17
|
+
// ── 2. Always-on shared packages ──
|
|
18
|
+
spin.start("Scaffolding shared packages…");
|
|
19
|
+
for (const pkg of ["api", "db", "auth", "validators", "config"]) {
|
|
20
|
+
await copyTemplate(path.join(TEMPLATES_DIR, "packages", pkg), path.join(destDir, "packages", pkg), vars);
|
|
21
|
+
}
|
|
22
|
+
spin.stop("Shared packages scaffolded");
|
|
23
|
+
// ── 3. Design system ──
|
|
24
|
+
if (config.designSystem) {
|
|
25
|
+
spin.start("Adding design system (@repo/ui)…");
|
|
26
|
+
await copyTemplate(path.join(TEMPLATES_DIR, "packages", "ui"), path.join(destDir, "packages", "ui"), vars);
|
|
27
|
+
spin.stop("Design system added");
|
|
28
|
+
}
|
|
29
|
+
// ── 4. Email ──
|
|
30
|
+
if (config.resend) {
|
|
31
|
+
spin.start("Adding Resend email package (@repo/email)…");
|
|
32
|
+
await copyTemplate(path.join(TEMPLATES_DIR, "packages", "email"), path.join(destDir, "packages", "email"), vars);
|
|
33
|
+
spin.stop("Email package added");
|
|
34
|
+
}
|
|
35
|
+
// ── 5. Web app ──
|
|
36
|
+
if (config.apps.includes("web")) {
|
|
37
|
+
spin.start("Scaffolding Next.js web app…");
|
|
38
|
+
await copyTemplate(path.join(TEMPLATES_DIR, "apps", "web"), path.join(destDir, "apps", "web"), vars);
|
|
39
|
+
if (!config.sentry) {
|
|
40
|
+
await removeDeps(path.join(destDir, "apps", "web"), ["@sentry/nextjs"]);
|
|
41
|
+
await fs.remove(path.join(destDir, "apps", "web", "sentry.client.config.ts"));
|
|
42
|
+
await fs.remove(path.join(destDir, "apps", "web", "sentry.server.config.ts"));
|
|
43
|
+
await fs.remove(path.join(destDir, "apps", "web", "sentry.edge.config.ts"));
|
|
44
|
+
}
|
|
45
|
+
if (!config.posthog) {
|
|
46
|
+
await removeDeps(path.join(destDir, "apps", "web"), ["posthog-js", "posthog-node"]);
|
|
47
|
+
}
|
|
48
|
+
if (!config.googleAnalytics) {
|
|
49
|
+
await removeDeps(path.join(destDir, "apps", "web"), ["@next/third-parties"]);
|
|
50
|
+
}
|
|
51
|
+
spin.stop("Web app scaffolded");
|
|
52
|
+
}
|
|
53
|
+
// ── 6. Mobile app ──
|
|
54
|
+
if (config.apps.includes("mobile")) {
|
|
55
|
+
spin.start("Scaffolding Expo mobile app…");
|
|
56
|
+
await copyTemplate(path.join(TEMPLATES_DIR, "apps", "mobile"), path.join(destDir, "apps", "mobile"), vars);
|
|
57
|
+
if (!config.sentry) {
|
|
58
|
+
await removeDeps(path.join(destDir, "apps", "mobile"), ["@sentry/react-native"]);
|
|
59
|
+
}
|
|
60
|
+
if (!config.posthog) {
|
|
61
|
+
await removeDeps(path.join(destDir, "apps", "mobile"), ["posthog-react-native"]);
|
|
62
|
+
}
|
|
63
|
+
spin.stop("Mobile app scaffolded");
|
|
64
|
+
}
|
|
65
|
+
// ── 7. Update pnpm-workspace.yaml based on selected apps ──
|
|
66
|
+
await updateWorkspace(destDir, config);
|
|
67
|
+
// ── 8. Install dependencies ──
|
|
68
|
+
spin.start(`Installing dependencies with ${config.packageManager}…`);
|
|
69
|
+
await installDeps(destDir, config.packageManager);
|
|
70
|
+
spin.stop("Dependencies installed");
|
|
71
|
+
// ── 9. Git init ──
|
|
72
|
+
spin.start("Initialising git repository…");
|
|
73
|
+
await gitInit(destDir);
|
|
74
|
+
spin.stop("Git repository initialised");
|
|
75
|
+
}
|
|
76
|
+
async function updateWorkspace(destDir, config) {
|
|
77
|
+
const workspacePath = path.join(destDir, "pnpm-workspace.yaml");
|
|
78
|
+
const lines = ["packages:"];
|
|
79
|
+
if (config.apps.includes("web") || config.apps.includes("mobile")) {
|
|
80
|
+
lines.push(' - "apps/*"');
|
|
81
|
+
}
|
|
82
|
+
lines.push(' - "packages/*"');
|
|
83
|
+
await fs.outputFile(workspacePath, lines.join("\n") + "\n", "utf-8");
|
|
84
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-pilotprojects-app",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CLI to scaffold the Pilotprojects monorepo boilerplate",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"create",
|
|
7
|
+
"monorepo",
|
|
8
|
+
"nextjs",
|
|
9
|
+
"expo",
|
|
10
|
+
"trpc",
|
|
11
|
+
"supabase"
|
|
12
|
+
],
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"type": "module",
|
|
15
|
+
"bin": {
|
|
16
|
+
"create-pilotprojects-app": "./dist/index.js"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist",
|
|
20
|
+
"templates"
|
|
21
|
+
],
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@clack/prompts": "^0.9.1",
|
|
24
|
+
"commander": "^12.1.0",
|
|
25
|
+
"execa": "^9.3.0",
|
|
26
|
+
"fs-extra": "^11.2.0",
|
|
27
|
+
"picocolors": "^1.1.0"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/fs-extra": "^11.0.4",
|
|
31
|
+
"@types/node": "^20.0.0",
|
|
32
|
+
"typescript": "^5.5.0"
|
|
33
|
+
},
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "tsc",
|
|
36
|
+
"dev": "tsc --watch",
|
|
37
|
+
"start": "node dist/index.js"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Set APP_ENV to: development | uat | production
|
|
2
|
+
APP_ENV=development
|
|
3
|
+
|
|
4
|
+
# Supabase
|
|
5
|
+
EXPO_PUBLIC_SUPABASE_URL=http://localhost:54321
|
|
6
|
+
EXPO_PUBLIC_SUPABASE_ANON_KEY=
|
|
7
|
+
|
|
8
|
+
# API (apps/web URL)
|
|
9
|
+
EXPO_PUBLIC_API_URL=http://localhost:3000
|
|
10
|
+
|
|
11
|
+
# Sentry (optional)
|
|
12
|
+
EXPO_PUBLIC_SENTRY_DSN=
|
|
13
|
+
|
|
14
|
+
# PostHog (optional)
|
|
15
|
+
EXPO_PUBLIC_POSTHOG_KEY=
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { View, Text, KeyboardAvoidingView, Platform, ScrollView, TouchableOpacity } from "react-native";
|
|
3
|
+
|
|
4
|
+
import { supabase } from "@/lib/supabase";
|
|
5
|
+
import { Button } from "{{PACKAGE_SCOPE}}/ui/native/button";
|
|
6
|
+
import { Input } from "{{PACKAGE_SCOPE}}/ui/native/input";
|
|
7
|
+
|
|
8
|
+
export default function LoginScreen() {
|
|
9
|
+
const [email, setEmail] = useState("");
|
|
10
|
+
const [password, setPassword] = useState("");
|
|
11
|
+
const [error, setError] = useState<string | null>(null);
|
|
12
|
+
const [loading, setLoading] = useState(false);
|
|
13
|
+
|
|
14
|
+
async function handleLogin() {
|
|
15
|
+
setLoading(true);
|
|
16
|
+
setError(null);
|
|
17
|
+
|
|
18
|
+
const { error } = await supabase.auth.signInWithPassword({ email, password });
|
|
19
|
+
|
|
20
|
+
if (error) setError(error.message);
|
|
21
|
+
setLoading(false);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<KeyboardAvoidingView
|
|
26
|
+
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
|
27
|
+
className="flex-1 bg-background"
|
|
28
|
+
>
|
|
29
|
+
<ScrollView
|
|
30
|
+
contentContainerStyle={{ flexGrow: 1 }}
|
|
31
|
+
keyboardShouldPersistTaps="handled"
|
|
32
|
+
>
|
|
33
|
+
<View className="flex-1 items-center justify-center px-6 py-16">
|
|
34
|
+
<View className="mb-10 items-center gap-2">
|
|
35
|
+
<View className="h-16 w-16 items-center justify-center rounded-2xl bg-primary">
|
|
36
|
+
<Text className="text-2xl font-bold text-primary-foreground">A</Text>
|
|
37
|
+
</View>
|
|
38
|
+
<Text className="text-2xl font-bold text-foreground">{{PROJECT_NAME}}</Text>
|
|
39
|
+
<Text className="text-sm text-muted-foreground">Sign in to continue</Text>
|
|
40
|
+
</View>
|
|
41
|
+
|
|
42
|
+
<View className="w-full gap-5">
|
|
43
|
+
<View className="gap-1.5">
|
|
44
|
+
<Text className="text-sm font-medium text-foreground">Email</Text>
|
|
45
|
+
<Input
|
|
46
|
+
placeholder="you@example.com"
|
|
47
|
+
value={email}
|
|
48
|
+
onChangeText={setEmail}
|
|
49
|
+
keyboardType="email-address"
|
|
50
|
+
autoCapitalize="none"
|
|
51
|
+
/>
|
|
52
|
+
</View>
|
|
53
|
+
|
|
54
|
+
<View className="gap-1.5">
|
|
55
|
+
<View className="flex-row items-center justify-between">
|
|
56
|
+
<Text className="text-sm font-medium text-foreground">Password</Text>
|
|
57
|
+
<TouchableOpacity>
|
|
58
|
+
<Text className="text-xs text-primary">Forgot password?</Text>
|
|
59
|
+
</TouchableOpacity>
|
|
60
|
+
</View>
|
|
61
|
+
<Input
|
|
62
|
+
placeholder="••••••••"
|
|
63
|
+
value={password}
|
|
64
|
+
onChangeText={setPassword}
|
|
65
|
+
secureTextEntry
|
|
66
|
+
/>
|
|
67
|
+
</View>
|
|
68
|
+
|
|
69
|
+
{error && <Text className="text-sm text-destructive">{error}</Text>}
|
|
70
|
+
|
|
71
|
+
<Button onPress={handleLogin} disabled={loading} size="lg">
|
|
72
|
+
{loading ? "Signing in…" : "Sign in"}
|
|
73
|
+
</Button>
|
|
74
|
+
</View>
|
|
75
|
+
|
|
76
|
+
<View className="mt-8 flex-row items-center gap-1">
|
|
77
|
+
<Text className="text-sm text-muted-foreground">Don't have an account?</Text>
|
|
78
|
+
<TouchableOpacity>
|
|
79
|
+
<Text className="text-sm font-medium text-primary">Sign up</Text>
|
|
80
|
+
</TouchableOpacity>
|
|
81
|
+
</View>
|
|
82
|
+
</View>
|
|
83
|
+
</ScrollView>
|
|
84
|
+
</KeyboardAvoidingView>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Tabs } from "expo-router";
|
|
2
|
+
|
|
3
|
+
export default function TabsLayout() {
|
|
4
|
+
return (
|
|
5
|
+
<Tabs screenOptions={{ tabBarActiveTintColor: "#3b82f6" }}>
|
|
6
|
+
<Tabs.Screen name="index" options={{ title: "Home" }} />
|
|
7
|
+
<Tabs.Screen name="profile" options={{ title: "Profile" }} />
|
|
8
|
+
</Tabs>
|
|
9
|
+
);
|
|
10
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { View, Text, ScrollView } from "react-native";
|
|
2
|
+
|
|
3
|
+
import { trpc } from "@/lib/trpc/client";
|
|
4
|
+
import { Card, CardHeader, CardTitle, CardContent } from "{{PACKAGE_SCOPE}}/ui/native/card";
|
|
5
|
+
|
|
6
|
+
export default function HomeScreen() {
|
|
7
|
+
const { data: user, isLoading } = trpc.user.me.useQuery();
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<ScrollView className="flex-1 bg-background">
|
|
11
|
+
<View className="p-6">
|
|
12
|
+
<Text className="mb-4 text-2xl font-bold text-foreground">Home</Text>
|
|
13
|
+
{isLoading ? (
|
|
14
|
+
<Text className="text-muted-foreground">Loading…</Text>
|
|
15
|
+
) : (
|
|
16
|
+
<Card>
|
|
17
|
+
<CardHeader>
|
|
18
|
+
<CardTitle>Welcome back</CardTitle>
|
|
19
|
+
</CardHeader>
|
|
20
|
+
<CardContent>
|
|
21
|
+
<Text className="text-muted-foreground">
|
|
22
|
+
{user?.displayName ?? user?.email}
|
|
23
|
+
</Text>
|
|
24
|
+
</CardContent>
|
|
25
|
+
</Card>
|
|
26
|
+
)}
|
|
27
|
+
</View>
|
|
28
|
+
</ScrollView>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import "../global.css";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import { Stack, useRouter, useSegments } from "expo-router";
|
|
5
|
+
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
6
|
+
import * as Sentry from "@sentry/react-native";
|
|
7
|
+
|
|
8
|
+
import { supabase } from "@/lib/supabase";
|
|
9
|
+
import { trpc, makeTRPCClient } from "@/lib/trpc/client";
|
|
10
|
+
|
|
11
|
+
if (process.env.EXPO_PUBLIC_SENTRY_DSN) {
|
|
12
|
+
Sentry.init({
|
|
13
|
+
dsn: process.env.EXPO_PUBLIC_SENTRY_DSN,
|
|
14
|
+
environment: process.env.APP_ENV ?? "development",
|
|
15
|
+
tracesSampleRate: 1.0,
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const queryClient = new QueryClient({
|
|
20
|
+
defaultOptions: { queries: { staleTime: 60_000 } },
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
function AuthGate({ children }: { children: React.ReactNode }) {
|
|
24
|
+
const router = useRouter();
|
|
25
|
+
const segments = useSegments();
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
const { data: { subscription } } = supabase.auth.onAuthStateChange((_event, session) => {
|
|
29
|
+
const inAuthGroup = segments[0] === "(auth)";
|
|
30
|
+
|
|
31
|
+
if (!session && !inAuthGroup) {
|
|
32
|
+
router.replace("/(auth)/login");
|
|
33
|
+
} else if (session && inAuthGroup) {
|
|
34
|
+
router.replace("/(tabs)");
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
return () => subscription.unsubscribe();
|
|
39
|
+
}, [segments]);
|
|
40
|
+
|
|
41
|
+
return <>{children}</>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function RootLayout() {
|
|
45
|
+
const [trpcClient] = useState(() => makeTRPCClient());
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<trpc.Provider client={trpcClient} queryClient={queryClient}>
|
|
49
|
+
<QueryClientProvider client={queryClient}>
|
|
50
|
+
<AuthGate>
|
|
51
|
+
<Stack screenOptions={{ headerShown: false }} />
|
|
52
|
+
</AuthGate>
|
|
53
|
+
</QueryClientProvider>
|
|
54
|
+
</trpc.Provider>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export default Sentry.wrap(RootLayout);
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { ExpoConfig, ConfigContext } from "expo/config";
|
|
2
|
+
|
|
3
|
+
type AppEnv = "development" | "uat" | "production";
|
|
4
|
+
|
|
5
|
+
const ENV = (process.env.APP_ENV ?? "development") as AppEnv;
|
|
6
|
+
|
|
7
|
+
const config: Record<AppEnv, { name: string; bundleId: string; apiUrl: string }> = {
|
|
8
|
+
development: {
|
|
9
|
+
name: "{{PROJECT_NAME}} (Dev)",
|
|
10
|
+
bundleId: "com.yourcompany.{{PACKAGE_SCOPE_SAFE}}.dev",
|
|
11
|
+
apiUrl: "http://localhost:3000",
|
|
12
|
+
},
|
|
13
|
+
uat: {
|
|
14
|
+
name: "{{PROJECT_NAME}} (UAT)",
|
|
15
|
+
bundleId: "com.yourcompany.{{PACKAGE_SCOPE_SAFE}}.uat",
|
|
16
|
+
apiUrl: "https://uat.yourapp.com",
|
|
17
|
+
},
|
|
18
|
+
production: {
|
|
19
|
+
name: "{{PROJECT_NAME}}",
|
|
20
|
+
bundleId: "com.yourcompany.{{PACKAGE_SCOPE_SAFE}}",
|
|
21
|
+
apiUrl: "https://yourapp.com",
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const { name, bundleId, apiUrl } = config[ENV];
|
|
26
|
+
|
|
27
|
+
export default ({ config: base }: ConfigContext): ExpoConfig => ({
|
|
28
|
+
...base,
|
|
29
|
+
name,
|
|
30
|
+
slug: "{{PACKAGE_SCOPE_SAFE}}",
|
|
31
|
+
version: "1.0.0",
|
|
32
|
+
orientation: "portrait",
|
|
33
|
+
scheme: "{{PACKAGE_SCOPE_SAFE}}",
|
|
34
|
+
userInterfaceStyle: "automatic",
|
|
35
|
+
icon: "./assets/icon.png",
|
|
36
|
+
splash: {
|
|
37
|
+
image: "./assets/splash-icon.png",
|
|
38
|
+
resizeMode: "contain",
|
|
39
|
+
backgroundColor: "#ffffff",
|
|
40
|
+
},
|
|
41
|
+
ios: {
|
|
42
|
+
bundleIdentifier: bundleId,
|
|
43
|
+
supportsTablet: true,
|
|
44
|
+
},
|
|
45
|
+
android: {
|
|
46
|
+
package: bundleId,
|
|
47
|
+
adaptiveIcon: {
|
|
48
|
+
foregroundImage: "./assets/adaptive-icon.png",
|
|
49
|
+
backgroundColor: "#ffffff",
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
web: {
|
|
53
|
+
bundler: "metro",
|
|
54
|
+
output: "static",
|
|
55
|
+
favicon: "./assets/favicon.png",
|
|
56
|
+
},
|
|
57
|
+
plugins: [
|
|
58
|
+
"expo-router",
|
|
59
|
+
"@sentry/react-native/expo",
|
|
60
|
+
],
|
|
61
|
+
newArchEnabled: true,
|
|
62
|
+
experiments: {
|
|
63
|
+
typedRoutes: true,
|
|
64
|
+
},
|
|
65
|
+
extra: {
|
|
66
|
+
apiUrl,
|
|
67
|
+
eas: { projectId: "YOUR_EAS_PROJECT_ID" },
|
|
68
|
+
},
|
|
69
|
+
updates: {
|
|
70
|
+
url: "https://u.expo.dev/YOUR_EAS_PROJECT_ID",
|
|
71
|
+
},
|
|
72
|
+
runtimeVersion: { policy: "sdkVersion" },
|
|
73
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"cli": { "version": ">= 12.0.0" },
|
|
3
|
+
"build": {
|
|
4
|
+
"development": {
|
|
5
|
+
"developmentClient": true,
|
|
6
|
+
"distribution": "internal",
|
|
7
|
+
"channel": "development",
|
|
8
|
+
"ios": { "simulator": true }
|
|
9
|
+
},
|
|
10
|
+
"uat": {
|
|
11
|
+
"distribution": "internal",
|
|
12
|
+
"channel": "uat",
|
|
13
|
+
"ios": {
|
|
14
|
+
"buildConfiguration": "Release",
|
|
15
|
+
"credentialsSource": "remote"
|
|
16
|
+
},
|
|
17
|
+
"android": {
|
|
18
|
+
"buildType": "apk",
|
|
19
|
+
"credentialsSource": "remote"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"production": {
|
|
23
|
+
"distribution": "store",
|
|
24
|
+
"channel": "production",
|
|
25
|
+
"ios": {
|
|
26
|
+
"buildConfiguration": "Release",
|
|
27
|
+
"credentialsSource": "remote"
|
|
28
|
+
},
|
|
29
|
+
"android": {
|
|
30
|
+
"buildType": "app-bundle",
|
|
31
|
+
"credentialsSource": "remote"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"submit": {
|
|
36
|
+
"production": {
|
|
37
|
+
"ios": {
|
|
38
|
+
"appleId": "",
|
|
39
|
+
"ascAppId": "",
|
|
40
|
+
"appleTeamId": ""
|
|
41
|
+
},
|
|
42
|
+
"android": {
|
|
43
|
+
"serviceAccountKeyPath": "./google-service-account.json",
|
|
44
|
+
"track": "internal"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|