create-pilotprojects-app 0.1.1 → 0.2.1

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/README.md CHANGED
@@ -55,17 +55,18 @@ The CLI is interactive — it asks a few questions then scaffolds, installs depe
55
55
 
56
56
  ## Interactive prompts
57
57
 
58
- | Prompt | Options | Default |
59
- | ------------------------------ | ------------------- | ----------------- |
60
- | Project name | Any lowercase slug | — |
61
- | Package scope | `@<name>` | `@<project-name>` |
62
- | Apps to scaffold | Web / Mobile / Both | Both |
63
- | Include design system? | Yes / No | Yes |
64
- | Include Sentry? | Yes / No | Yes |
65
- | Include Resend (email)? | Yes / No | Yes |
66
- | Include PostHog? | Yes / No | No |
67
- | Google Analytics? _(web only)_ | Yes / No | No |
68
- | Package manager | pnpm / npm / yarn | pnpm |
58
+ | Prompt | Options | Default |
59
+ | ------------------------------ | -------------------------------------------------------- | ----------------- |
60
+ | Project name | Any lowercase slug | — |
61
+ | Package scope | `@<name>` | `@<project-name>` |
62
+ | Apps to scaffold | Web / Mobile / Both | Both |
63
+ | Environments | development / uat / production _(local always included)_ | production |
64
+ | Include design system? | Yes / No | Yes |
65
+ | Include Sentry? | Yes / No | Yes |
66
+ | Include Resend (email)? | Yes / No | Yes |
67
+ | Include PostHog? | Yes / No | No |
68
+ | Google Analytics? _(web only)_ | Yes / No | No |
69
+ | Package manager | pnpm / npm / yarn | pnpm |
69
70
 
70
71
  ---
71
72
 
package/dist/prompts.js CHANGED
@@ -34,6 +34,16 @@ export async function runPrompts(projectNameArg) {
34
34
  initialValues: ["web", "mobile"],
35
35
  required: true,
36
36
  }),
37
+ environments: () => p.multiselect({
38
+ message: "Which environments? (local is always included)",
39
+ options: [
40
+ { value: "development", label: "Development", hint: "dev branch" },
41
+ { value: "uat", label: "UAT", hint: "staging / QA" },
42
+ { value: "production", label: "Production", hint: "live users" },
43
+ ],
44
+ initialValues: ["production"],
45
+ required: true,
46
+ }),
37
47
  designSystem: () => p.confirm({
38
48
  message: "Include the default design system? (Tailwind + ShadCN web / NativeWind mobile)",
39
49
  initialValue: true,
package/dist/scaffold.js CHANGED
@@ -36,6 +36,7 @@ export async function scaffold(config, destDir) {
36
36
  if (config.apps.includes("web")) {
37
37
  spin.start("Scaffolding Next.js web app…");
38
38
  await copyTemplate(path.join(TEMPLATES_DIR, "apps", "web"), path.join(destDir, "apps", "web"), vars);
39
+ await generateWebEnvFiles(path.join(destDir, "apps", "web"), config);
39
40
  if (!config.sentry) {
40
41
  await removeDeps(path.join(destDir, "apps", "web"), ["@sentry/nextjs"]);
41
42
  await fs.remove(path.join(destDir, "apps", "web", "sentry.client.config.ts"));
@@ -54,6 +55,9 @@ export async function scaffold(config, destDir) {
54
55
  if (config.apps.includes("mobile")) {
55
56
  spin.start("Scaffolding Expo mobile app…");
56
57
  await copyTemplate(path.join(TEMPLATES_DIR, "apps", "mobile"), path.join(destDir, "apps", "mobile"), vars);
58
+ await generateMobileEnvFiles(path.join(destDir, "apps", "mobile"), config);
59
+ await generateEasJson(path.join(destDir, "apps", "mobile"), config.environments);
60
+ await generateMobileAppConfig(path.join(destDir, "apps", "mobile"), config);
57
61
  if (!config.sentry) {
58
62
  await removeDeps(path.join(destDir, "apps", "mobile"), ["@sentry/react-native"]);
59
63
  }
@@ -82,3 +86,186 @@ async function updateWorkspace(destDir, config) {
82
86
  lines.push(' - "packages/*"');
83
87
  await fs.outputFile(workspacePath, lines.join("\n") + "\n", "utf-8");
84
88
  }
89
+ // ── Environment file generators ──────────────────────────────────────────────
90
+ const WEB_ENV_BASE = `# Supabase
91
+ DATABASE_URL=
92
+ SUPABASE_URL=
93
+ SUPABASE_ANON_KEY=
94
+ SUPABASE_SERVICE_ROLE_KEY=
95
+ NEXT_PUBLIC_SUPABASE_URL=
96
+ NEXT_PUBLIC_SUPABASE_ANON_KEY=
97
+
98
+ # Sentry (optional — leave empty to disable)
99
+ SENTRY_DSN=
100
+ SENTRY_AUTH_TOKEN=
101
+ NEXT_PUBLIC_SENTRY_DSN=
102
+
103
+ # PostHog (optional)
104
+ NEXT_PUBLIC_POSTHOG_KEY=
105
+ POSTHOG_KEY=
106
+
107
+ # Google Analytics (optional)
108
+ NEXT_PUBLIC_GA_MEASUREMENT_ID=
109
+
110
+ # App
111
+ APP_URL=`;
112
+ const MOBILE_ENV_BASE = `# Supabase
113
+ EXPO_PUBLIC_SUPABASE_URL=
114
+ EXPO_PUBLIC_SUPABASE_ANON_KEY=
115
+
116
+ # API (apps/web URL)
117
+ EXPO_PUBLIC_API_URL=
118
+
119
+ # Sentry (optional)
120
+ EXPO_PUBLIC_SENTRY_DSN=
121
+
122
+ # PostHog (optional)
123
+ EXPO_PUBLIC_POSTHOG_KEY=
124
+ `;
125
+ const WEB_APP_URL = {
126
+ development: "https://dev.yourapp.com",
127
+ uat: "https://uat.yourapp.com",
128
+ production: "https://yourapp.com",
129
+ };
130
+ const MOBILE_API_URL = {
131
+ development: "https://dev.yourapp.com",
132
+ uat: "https://uat.yourapp.com",
133
+ production: "https://yourapp.com",
134
+ };
135
+ async function generateWebEnvFiles(webDir, config) {
136
+ // .env.local.example is already copied from template (localhost values)
137
+ // generate one .env.{env}.example per selected non-local environment
138
+ for (const env of config.environments) {
139
+ const content = `${WEB_ENV_BASE}${WEB_APP_URL[env]}\n`;
140
+ await fs.outputFile(path.join(webDir, `.env.${env}.example`), content, "utf-8");
141
+ }
142
+ }
143
+ async function generateMobileEnvFiles(mobileDir, config) {
144
+ // .env.local.example is already copied from template (localhost values)
145
+ // generate one .env.{env}.example per selected non-local environment
146
+ for (const env of config.environments) {
147
+ const content = `APP_ENV=${env}\n\n${MOBILE_ENV_BASE}`;
148
+ const withUrl = content.replace("EXPO_PUBLIC_API_URL=", `EXPO_PUBLIC_API_URL=${MOBILE_API_URL[env]}`);
149
+ await fs.outputFile(path.join(mobileDir, `.env.${env}.example`), withUrl, "utf-8");
150
+ }
151
+ }
152
+ async function generateEasJson(mobileDir, environments) {
153
+ const build = {};
154
+ if (environments.includes("development")) {
155
+ build.development = {
156
+ developmentClient: true,
157
+ distribution: "internal",
158
+ channel: "development",
159
+ ios: { simulator: true },
160
+ };
161
+ }
162
+ if (environments.includes("uat")) {
163
+ build.uat = {
164
+ distribution: "internal",
165
+ channel: "uat",
166
+ ios: { buildConfiguration: "Release", credentialsSource: "remote" },
167
+ android: { buildType: "apk", credentialsSource: "remote" },
168
+ };
169
+ }
170
+ if (environments.includes("production")) {
171
+ build.production = {
172
+ distribution: "store",
173
+ channel: "production",
174
+ ios: { buildConfiguration: "Release", credentialsSource: "remote" },
175
+ android: { buildType: "app-bundle", credentialsSource: "remote" },
176
+ };
177
+ }
178
+ const submit = environments.includes("production")
179
+ ? {
180
+ production: {
181
+ ios: { appleId: "", ascAppId: "", appleTeamId: "" },
182
+ android: { serviceAccountKeyPath: "./google-service-account.json", track: "internal" },
183
+ },
184
+ }
185
+ : {};
186
+ await fs.outputJSON(path.join(mobileDir, "eas.json"), { cli: { version: ">= 12.0.0" }, build, submit }, { spaces: 2 });
187
+ }
188
+ async function generateMobileAppConfig(mobileDir, config) {
189
+ const { projectName, packageScope, environments, sentry } = config;
190
+ const scopeSafe = packageScope.replace("@", "").replace("/", "-");
191
+ const allEnvs = ["local", ...environments];
192
+ const appEnvType = allEnvs.map((e) => `"${e}"`).join(" | ");
193
+ const defaultEnv = "local";
194
+ const envMeta = {
195
+ local: { label: " (Local)", bundleSuffix: ".local", apiUrl: "http://localhost:3000" },
196
+ development: { label: " (Dev)", bundleSuffix: ".dev", apiUrl: "https://dev.yourapp.com" },
197
+ uat: { label: " (UAT)", bundleSuffix: ".uat", apiUrl: "https://uat.yourapp.com" },
198
+ production: { label: "", bundleSuffix: "", apiUrl: "https://yourapp.com" },
199
+ };
200
+ const configEntries = allEnvs
201
+ .map((env) => {
202
+ const { label, bundleSuffix, apiUrl } = envMeta[env];
203
+ return ` ${env}: {
204
+ name: "${projectName}${label}",
205
+ bundleId: "com.yourcompany.${scopeSafe}${bundleSuffix}",
206
+ apiUrl: "${apiUrl}",
207
+ },`;
208
+ })
209
+ .join("\n");
210
+ const sentryPlugin = sentry ? `\n "@sentry/react-native/expo",` : "";
211
+ const content = `import type { ExpoConfig, ConfigContext } from "expo/config";
212
+
213
+ type AppEnv = ${appEnvType};
214
+
215
+ const ENV = (process.env.APP_ENV ?? "${defaultEnv}") as AppEnv;
216
+
217
+ const config: Record<AppEnv, { name: string; bundleId: string; apiUrl: string }> = {
218
+ ${configEntries}
219
+ };
220
+
221
+ const { name, bundleId, apiUrl } = config[ENV];
222
+
223
+ export default ({ config: base }: ConfigContext): ExpoConfig => ({
224
+ ...base,
225
+ name,
226
+ slug: "${scopeSafe}",
227
+ version: "1.0.0",
228
+ orientation: "portrait",
229
+ scheme: "${scopeSafe}",
230
+ userInterfaceStyle: "automatic",
231
+ icon: "./assets/icon.png",
232
+ splash: {
233
+ image: "./assets/splash-icon.png",
234
+ resizeMode: "contain",
235
+ backgroundColor: "#ffffff",
236
+ },
237
+ ios: {
238
+ bundleIdentifier: bundleId,
239
+ supportsTablet: true,
240
+ },
241
+ android: {
242
+ package: bundleId,
243
+ adaptiveIcon: {
244
+ foregroundImage: "./assets/adaptive-icon.png",
245
+ backgroundColor: "#ffffff",
246
+ },
247
+ },
248
+ web: {
249
+ bundler: "metro",
250
+ output: "static",
251
+ favicon: "./assets/favicon.png",
252
+ },
253
+ plugins: [
254
+ "expo-router",${sentryPlugin}
255
+ ],
256
+ newArchEnabled: true,
257
+ experiments: {
258
+ typedRoutes: true,
259
+ },
260
+ extra: {
261
+ apiUrl,
262
+ eas: { projectId: "YOUR_EAS_PROJECT_ID" },
263
+ },
264
+ updates: {
265
+ url: "https://u.expo.dev/YOUR_EAS_PROJECT_ID",
266
+ },
267
+ runtimeVersion: { policy: "sdkVersion" },
268
+ });
269
+ `;
270
+ await fs.outputFile(path.join(mobileDir, "app.config.ts"), content, "utf-8");
271
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-pilotprojects-app",
3
- "version": "0.1.1",
3
+ "version": "0.2.1",
4
4
  "description": "CLI to scaffold the Pilotprojects monorepo boilerplate",
5
5
  "keywords": [
6
6
  "create",
@@ -1,5 +1,4 @@
1
- # Set APP_ENV to: development | uat | production
2
- APP_ENV=development
1
+ APP_ENV=local
3
2
 
4
3
  # Supabase
5
4
  EXPO_PUBLIC_SUPABASE_URL=http://localhost:54321