@codaijs/keel 0.2.3 → 0.2.4

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 (80) hide show
  1. package/dist/__tests__/sail-installer.test.js +25 -25
  2. package/dist/sail-installer.js +174 -174
  3. package/dist/scaffold.js +68 -68
  4. package/package.json +58 -58
  5. package/sails/_template/addon.json +20 -20
  6. package/sails/_template/install.ts +402 -402
  7. package/sails/admin-dashboard/README.md +117 -117
  8. package/sails/admin-dashboard/addon.json +28 -28
  9. package/sails/admin-dashboard/files/backend/middleware/admin.ts +34 -34
  10. package/sails/admin-dashboard/files/backend/routes/admin.ts +243 -243
  11. package/sails/admin-dashboard/files/frontend/components/admin/StatsCard.tsx +40 -40
  12. package/sails/admin-dashboard/files/frontend/components/admin/UsersTable.tsx +240 -240
  13. package/sails/admin-dashboard/files/frontend/hooks/useAdmin.ts +149 -149
  14. package/sails/admin-dashboard/files/frontend/pages/admin/Dashboard.tsx +173 -173
  15. package/sails/admin-dashboard/files/frontend/pages/admin/UserDetail.tsx +203 -203
  16. package/sails/admin-dashboard/install.ts +305 -305
  17. package/sails/analytics/README.md +178 -178
  18. package/sails/analytics/addon.json +27 -27
  19. package/sails/analytics/files/frontend/components/AnalyticsProvider.tsx +58 -58
  20. package/sails/analytics/files/frontend/hooks/useAnalytics.ts +64 -64
  21. package/sails/analytics/files/frontend/lib/analytics.ts +103 -103
  22. package/sails/analytics/install.ts +297 -297
  23. package/sails/file-uploads/addon.json +30 -30
  24. package/sails/file-uploads/files/backend/routes/files.ts +198 -198
  25. package/sails/file-uploads/files/backend/schema/files.ts +36 -36
  26. package/sails/file-uploads/files/backend/services/file-storage.ts +128 -128
  27. package/sails/file-uploads/files/frontend/components/FileList.tsx +248 -248
  28. package/sails/file-uploads/files/frontend/components/FileUploadButton.tsx +147 -147
  29. package/sails/file-uploads/files/frontend/hooks/useFileUpload.ts +106 -106
  30. package/sails/file-uploads/files/frontend/hooks/useFiles.ts +118 -118
  31. package/sails/file-uploads/files/frontend/pages/Files.tsx +37 -37
  32. package/sails/file-uploads/install.ts +466 -466
  33. package/sails/gdpr/README.md +174 -174
  34. package/sails/gdpr/addon.json +27 -27
  35. package/sails/gdpr/files/backend/routes/gdpr.ts +140 -140
  36. package/sails/gdpr/files/backend/services/gdpr.ts +293 -293
  37. package/sails/gdpr/files/frontend/components/auth/ConsentCheckboxes.tsx +97 -97
  38. package/sails/gdpr/files/frontend/components/gdpr/AccountDeletionRequest.tsx +192 -192
  39. package/sails/gdpr/files/frontend/components/gdpr/DataExportButton.tsx +75 -75
  40. package/sails/gdpr/files/frontend/pages/PrivacyPolicy.tsx +186 -186
  41. package/sails/gdpr/install.ts +756 -756
  42. package/sails/google-oauth/README.md +121 -121
  43. package/sails/google-oauth/addon.json +22 -22
  44. package/sails/google-oauth/files/GoogleButton.tsx +50 -50
  45. package/sails/google-oauth/install.ts +252 -252
  46. package/sails/i18n/README.md +193 -193
  47. package/sails/i18n/addon.json +30 -30
  48. package/sails/i18n/files/frontend/components/LanguageSwitcher.tsx +108 -108
  49. package/sails/i18n/files/frontend/hooks/useLanguage.ts +31 -31
  50. package/sails/i18n/files/frontend/lib/i18n.ts +32 -32
  51. package/sails/i18n/files/frontend/locales/de/common.json +44 -44
  52. package/sails/i18n/files/frontend/locales/en/common.json +44 -44
  53. package/sails/i18n/install.ts +407 -407
  54. package/sails/push-notifications/README.md +163 -163
  55. package/sails/push-notifications/addon.json +31 -31
  56. package/sails/push-notifications/files/backend/routes/notifications.ts +153 -153
  57. package/sails/push-notifications/files/backend/schema/notifications.ts +31 -31
  58. package/sails/push-notifications/files/backend/services/notifications.ts +117 -117
  59. package/sails/push-notifications/files/frontend/components/PushNotificationInit.tsx +12 -12
  60. package/sails/push-notifications/files/frontend/hooks/usePushNotifications.ts +154 -154
  61. package/sails/push-notifications/install.ts +384 -384
  62. package/sails/r2-storage/addon.json +29 -29
  63. package/sails/r2-storage/files/backend/services/storage.ts +71 -71
  64. package/sails/r2-storage/files/frontend/components/ProfilePictureUpload.tsx +167 -167
  65. package/sails/r2-storage/install.ts +412 -412
  66. package/sails/rate-limiting/addon.json +20 -20
  67. package/sails/rate-limiting/files/backend/middleware/rate-limit-store.ts +104 -104
  68. package/sails/rate-limiting/files/backend/middleware/rate-limit.ts +137 -137
  69. package/sails/rate-limiting/install.ts +300 -300
  70. package/sails/registry.json +107 -107
  71. package/sails/stripe/README.md +214 -214
  72. package/sails/stripe/addon.json +24 -24
  73. package/sails/stripe/files/backend/routes/stripe.ts +154 -154
  74. package/sails/stripe/files/backend/schema/stripe.ts +74 -74
  75. package/sails/stripe/files/backend/services/stripe.ts +224 -224
  76. package/sails/stripe/files/frontend/components/SubscriptionStatus.tsx +135 -135
  77. package/sails/stripe/files/frontend/hooks/useSubscription.ts +86 -86
  78. package/sails/stripe/files/frontend/pages/Checkout.tsx +116 -116
  79. package/sails/stripe/files/frontend/pages/Pricing.tsx +226 -226
  80. package/sails/stripe/install.ts +378 -378
@@ -1,297 +1,297 @@
1
- /**
2
- * PostHog Analytics Sail Installer
3
- *
4
- * Adds privacy-friendly analytics with PostHog — automatic page views,
5
- * user identification, and custom event tracking.
6
- *
7
- * PostHog can be used as a cloud service or self-hosted.
8
- *
9
- * Usage:
10
- * npx tsx sails/analytics/install.ts
11
- */
12
-
13
- import {
14
- readFileSync,
15
- writeFileSync,
16
- copyFileSync,
17
- existsSync,
18
- mkdirSync,
19
- } from "node:fs";
20
- import { resolve, dirname, join } from "node:path";
21
- import { execSync } from "node:child_process";
22
- import { input, confirm, select } from "@inquirer/prompts";
23
-
24
- // ---------------------------------------------------------------------------
25
- // Paths
26
- // ---------------------------------------------------------------------------
27
-
28
- const SAIL_DIR = dirname(new URL(import.meta.url).pathname);
29
- const PROJECT_ROOT = resolve(SAIL_DIR, "../..");
30
- const FRONTEND_ROOT = join(PROJECT_ROOT, "packages/frontend");
31
-
32
- // ---------------------------------------------------------------------------
33
- // Helpers
34
- // ---------------------------------------------------------------------------
35
-
36
- interface SailManifest {
37
- name: string;
38
- displayName: string;
39
- version: string;
40
- requiredEnvVars: { key: string; description: string }[];
41
- dependencies: { backend: Record<string, string>; frontend: Record<string, string> };
42
- }
43
-
44
- function loadManifest(): SailManifest {
45
- return JSON.parse(readFileSync(join(SAIL_DIR, "addon.json"), "utf-8"));
46
- }
47
-
48
- function copyFile(src: string, dest: string, label: string): void {
49
- mkdirSync(dirname(dest), { recursive: true });
50
- copyFileSync(src, dest);
51
- console.log(` Copied -> ${label}`);
52
- }
53
-
54
- function appendToEnvFiles(section: string, entries: Record<string, string>): void {
55
- for (const envFile of [".env.example", ".env"]) {
56
- const envPath = join(PROJECT_ROOT, envFile);
57
- if (!existsSync(envPath)) continue;
58
- let content = readFileSync(envPath, "utf-8");
59
- const lines: string[] = [];
60
- for (const [key, val] of Object.entries(entries)) {
61
- if (!content.includes(key)) lines.push(`${key}=${val}`);
62
- }
63
- if (lines.length > 0) {
64
- content += `\n# ${section}\n${lines.join("\n")}\n`;
65
- writeFileSync(envPath, content, "utf-8");
66
- console.log(` Updated ${envFile}`);
67
- }
68
- }
69
- }
70
-
71
- function installDeps(deps: Record<string, string>, workspace: string): void {
72
- const entries = Object.entries(deps);
73
- if (entries.length === 0) return;
74
- const packages = entries.map(([n, v]) => `${n}@${v}`).join(" ");
75
- const cmd = `npm install ${packages} --workspace=${workspace}`;
76
- console.log(` Running: ${cmd}`);
77
- execSync(cmd, { cwd: PROJECT_ROOT, stdio: "inherit" });
78
- }
79
-
80
- // ---------------------------------------------------------------------------
81
- // Main
82
- // ---------------------------------------------------------------------------
83
-
84
- async function main(): Promise<void> {
85
- const manifest = loadManifest();
86
-
87
- // -- Step 1: Welcome --------------------------------------------------------
88
- console.log("\n------------------------------------------------------------");
89
- console.log(` PostHog Analytics Sail Installer (v${manifest.version})`);
90
- console.log("------------------------------------------------------------");
91
- console.log();
92
- console.log(" This sail adds PostHog analytics to your app:");
93
- console.log(" - Automatic page view tracking on SPA route changes");
94
- console.log(" - User identification tied to your auth system");
95
- console.log(" - Custom event tracking for feature usage");
96
- console.log(" - Session recording and heatmaps (configurable in PostHog)");
97
- console.log();
98
- console.log(" PostHog is privacy-friendly, open-source, and GDPR-compatible.");
99
- console.log(" You can use PostHog Cloud or self-host it.");
100
- console.log();
101
-
102
- const pkgPath = join(PROJECT_ROOT, "package.json");
103
- if (existsSync(pkgPath)) {
104
- const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
105
- console.log(` Template version: ${pkg.version ?? "unknown"}`);
106
- console.log();
107
- }
108
-
109
- // -- Step 2: Hosting choice -------------------------------------------------
110
- const hostingChoice = await select({
111
- message: "How will you use PostHog?",
112
- choices: [
113
- {
114
- name: "PostHog Cloud (US)",
115
- value: "cloud-us",
116
- description: "Hosted by PostHog at us.i.posthog.com — easiest setup",
117
- },
118
- {
119
- name: "PostHog Cloud (EU)",
120
- value: "cloud-eu",
121
- description: "Hosted by PostHog at eu.i.posthog.com — EU data residency",
122
- },
123
- {
124
- name: "Self-hosted",
125
- value: "self-hosted",
126
- description: "Your own PostHog instance — full data control",
127
- },
128
- ],
129
- });
130
-
131
- let posthogHost: string;
132
-
133
- if (hostingChoice === "cloud-us") {
134
- posthogHost = "https://us.i.posthog.com";
135
- console.log();
136
- console.log(" To get your API key:");
137
- console.log(" 1. Sign up or log in at https://app.posthog.com");
138
- console.log(" 2. Create a project (or select existing)");
139
- console.log(" 3. Go to Project Settings");
140
- console.log(" 4. Copy the Project API Key");
141
- console.log();
142
- } else if (hostingChoice === "cloud-eu") {
143
- posthogHost = "https://eu.i.posthog.com";
144
- console.log();
145
- console.log(" To get your API key:");
146
- console.log(" 1. Sign up or log in at https://eu.posthog.com");
147
- console.log(" 2. Create a project (or select existing)");
148
- console.log(" 3. Go to Project Settings");
149
- console.log(" 4. Copy the Project API Key");
150
- console.log();
151
- } else {
152
- console.log();
153
- posthogHost = await input({
154
- message: "PostHog instance URL (e.g., https://posthog.yourdomain.com):",
155
- validate: (value) => {
156
- if (!value || value.trim().length === 0) return "Host URL is required.";
157
- if (!value.startsWith("http")) return "Should start with http:// or https://";
158
- return true;
159
- },
160
- });
161
- posthogHost = posthogHost.replace(/\/+$/, ""); // Remove trailing slashes
162
- console.log();
163
- }
164
-
165
- // -- Step 3: Collect API key ------------------------------------------------
166
- const posthogKey = await input({
167
- message: "PostHog Project API Key:",
168
- validate: (value) => {
169
- if (!value || value.trim().length === 0) return "API key is required.";
170
- if (value.startsWith("phx_")) return "This looks like a personal API key. Use the Project API key (starts with 'phc_').";
171
- return true;
172
- },
173
- });
174
-
175
- // -- Step 4: Summary --------------------------------------------------------
176
- console.log();
177
- console.log(" Summary of changes:");
178
- console.log(" -------------------");
179
- console.log(" Files to create (frontend):");
180
- console.log(" + packages/frontend/src/lib/analytics.ts");
181
- console.log(" + packages/frontend/src/hooks/useAnalytics.ts");
182
- console.log(" + packages/frontend/src/components/AnalyticsProvider.tsx");
183
- console.log();
184
- console.log(" Files to modify:");
185
- console.log(" ~ packages/frontend/src/App.tsx");
186
- console.log(" ~ .env.example / .env");
187
- console.log();
188
- console.log(" Environment variables:");
189
- console.log(` VITE_POSTHOG_KEY=${posthogKey.slice(0, 12)}...`);
190
- console.log(` VITE_POSTHOG_HOST=${posthogHost}`);
191
- console.log();
192
-
193
- // -- Step 5: Confirm --------------------------------------------------------
194
- const proceed = await confirm({ message: "Proceed with installation?", default: true });
195
- if (!proceed) { console.log("\n Installation cancelled.\n"); process.exit(0); }
196
-
197
- console.log();
198
- console.log(" Installing...");
199
- console.log();
200
-
201
- // -- Step 6: Copy files and modify App.tsx ----------------------------------
202
- console.log(" Copying frontend files...");
203
- const frontendFiles = [
204
- { src: "frontend/lib/analytics.ts", dest: "src/lib/analytics.ts" },
205
- { src: "frontend/hooks/useAnalytics.ts", dest: "src/hooks/useAnalytics.ts" },
206
- { src: "frontend/components/AnalyticsProvider.tsx", dest: "src/components/AnalyticsProvider.tsx" },
207
- ];
208
- for (const f of frontendFiles) {
209
- copyFile(join(SAIL_DIR, "files", f.src), join(FRONTEND_ROOT, f.dest), f.dest);
210
- }
211
-
212
- console.log();
213
- console.log(" Modifying App.tsx...");
214
-
215
- const appPath = join(FRONTEND_ROOT, "src/App.tsx");
216
- if (existsSync(appPath)) {
217
- let appContent = readFileSync(appPath, "utf-8");
218
-
219
- if (!appContent.includes("AnalyticsProvider")) {
220
- // Add import
221
- const importLine = 'import { AnalyticsProvider } from "./components/AnalyticsProvider.js";';
222
- // Find the last import line and add after it
223
- const lastImport = appContent.lastIndexOf("import ");
224
- const importLineEnd = appContent.indexOf("\n", lastImport);
225
- appContent =
226
- appContent.slice(0, importLineEnd + 1) +
227
- importLine + "\n" +
228
- appContent.slice(importLineEnd + 1);
229
-
230
- // Wrap <AppRouter /> with <AnalyticsProvider>
231
- appContent = appContent.replace(
232
- "<AppRouter />",
233
- "<AnalyticsProvider>\n <AppRouter />\n </AnalyticsProvider>",
234
- );
235
-
236
- writeFileSync(appPath, appContent, "utf-8");
237
- console.log(" Modified -> App.tsx");
238
- } else {
239
- console.log(" Skipped (already present) -> App.tsx");
240
- }
241
- } else {
242
- console.warn(" Warning: App.tsx not found. Wrap your app with <AnalyticsProvider> manually.");
243
- }
244
-
245
- console.log();
246
- console.log(" Installing dependencies...");
247
- installDeps(manifest.dependencies.frontend, "packages/frontend");
248
-
249
- console.log();
250
- console.log(" Updating environment files...");
251
- appendToEnvFiles("PostHog Analytics", {
252
- VITE_POSTHOG_KEY: posthogKey.trim(),
253
- VITE_POSTHOG_HOST: posthogHost,
254
- });
255
-
256
- // -- Step 7: Next steps ------------------------------------------------------
257
- console.log();
258
- console.log("------------------------------------------------------------");
259
- console.log(" PostHog Analytics installed successfully!");
260
- console.log("------------------------------------------------------------");
261
- console.log();
262
- console.log(" Next steps:");
263
- console.log();
264
- console.log(" 1. Start your dev server:");
265
- console.log(" npm run dev");
266
- console.log();
267
- console.log(" 2. Verify analytics is working:");
268
- console.log(" - Open your app in the browser");
269
- console.log(" - Log in and navigate between pages");
270
- console.log(" - Check PostHog dashboard for events");
271
- console.log();
272
- console.log(" 3. Configure PostHog features (optional):");
273
- console.log(" - Session Recording: enable in PostHog project settings");
274
- console.log(" - Feature Flags: use posthog.isFeatureEnabled('flag-name')");
275
- console.log(" - Surveys: create in-app surveys from the PostHog dashboard");
276
- console.log();
277
- console.log(" 4. Track custom events in your components:");
278
- console.log();
279
- console.log(" import { useAnalytics } from '@/hooks/useAnalytics';");
280
- console.log();
281
- console.log(" function MyComponent() {");
282
- console.log(" const { trackEvent } = useAnalytics();");
283
- console.log(" return (");
284
- console.log(" <button onClick={() => trackEvent('button_clicked', { label: 'cta' })}>");
285
- console.log(" Click me");
286
- console.log(" </button>");
287
- console.log(" );");
288
- console.log(" }");
289
- console.log();
290
- console.log(" GDPR note:");
291
- console.log(" PostHog supports cookie-less tracking and consent management.");
292
- console.log(" If you have the GDPR sail installed, consider integrating");
293
- console.log(" analytics consent with your consent tracking system.");
294
- console.log();
295
- }
296
-
297
- main().catch((err) => { console.error("Installation failed:", err); process.exit(1); });
1
+ /**
2
+ * PostHog Analytics Sail Installer
3
+ *
4
+ * Adds privacy-friendly analytics with PostHog — automatic page views,
5
+ * user identification, and custom event tracking.
6
+ *
7
+ * PostHog can be used as a cloud service or self-hosted.
8
+ *
9
+ * Usage:
10
+ * npx tsx sails/analytics/install.ts
11
+ */
12
+
13
+ import {
14
+ readFileSync,
15
+ writeFileSync,
16
+ copyFileSync,
17
+ existsSync,
18
+ mkdirSync,
19
+ } from "node:fs";
20
+ import { resolve, dirname, join } from "node:path";
21
+ import { execSync } from "node:child_process";
22
+ import { input, confirm, select } from "@inquirer/prompts";
23
+
24
+ // ---------------------------------------------------------------------------
25
+ // Paths
26
+ // ---------------------------------------------------------------------------
27
+
28
+ const SAIL_DIR = dirname(new URL(import.meta.url).pathname);
29
+ const PROJECT_ROOT = resolve(SAIL_DIR, "../..");
30
+ const FRONTEND_ROOT = join(PROJECT_ROOT, "packages/frontend");
31
+
32
+ // ---------------------------------------------------------------------------
33
+ // Helpers
34
+ // ---------------------------------------------------------------------------
35
+
36
+ interface SailManifest {
37
+ name: string;
38
+ displayName: string;
39
+ version: string;
40
+ requiredEnvVars: { key: string; description: string }[];
41
+ dependencies: { backend: Record<string, string>; frontend: Record<string, string> };
42
+ }
43
+
44
+ function loadManifest(): SailManifest {
45
+ return JSON.parse(readFileSync(join(SAIL_DIR, "addon.json"), "utf-8"));
46
+ }
47
+
48
+ function copyFile(src: string, dest: string, label: string): void {
49
+ mkdirSync(dirname(dest), { recursive: true });
50
+ copyFileSync(src, dest);
51
+ console.log(` Copied -> ${label}`);
52
+ }
53
+
54
+ function appendToEnvFiles(section: string, entries: Record<string, string>): void {
55
+ for (const envFile of [".env.example", ".env"]) {
56
+ const envPath = join(PROJECT_ROOT, envFile);
57
+ if (!existsSync(envPath)) continue;
58
+ let content = readFileSync(envPath, "utf-8");
59
+ const lines: string[] = [];
60
+ for (const [key, val] of Object.entries(entries)) {
61
+ if (!content.includes(key)) lines.push(`${key}=${val}`);
62
+ }
63
+ if (lines.length > 0) {
64
+ content += `\n# ${section}\n${lines.join("\n")}\n`;
65
+ writeFileSync(envPath, content, "utf-8");
66
+ console.log(` Updated ${envFile}`);
67
+ }
68
+ }
69
+ }
70
+
71
+ function installDeps(deps: Record<string, string>, workspace: string): void {
72
+ const entries = Object.entries(deps);
73
+ if (entries.length === 0) return;
74
+ const packages = entries.map(([n, v]) => `${n}@${v}`).join(" ");
75
+ const cmd = `npm install ${packages} --workspace=${workspace}`;
76
+ console.log(` Running: ${cmd}`);
77
+ execSync(cmd, { cwd: PROJECT_ROOT, stdio: "inherit" });
78
+ }
79
+
80
+ // ---------------------------------------------------------------------------
81
+ // Main
82
+ // ---------------------------------------------------------------------------
83
+
84
+ async function main(): Promise<void> {
85
+ const manifest = loadManifest();
86
+
87
+ // -- Step 1: Welcome --------------------------------------------------------
88
+ console.log("\n------------------------------------------------------------");
89
+ console.log(` PostHog Analytics Sail Installer (v${manifest.version})`);
90
+ console.log("------------------------------------------------------------");
91
+ console.log();
92
+ console.log(" This sail adds PostHog analytics to your app:");
93
+ console.log(" - Automatic page view tracking on SPA route changes");
94
+ console.log(" - User identification tied to your auth system");
95
+ console.log(" - Custom event tracking for feature usage");
96
+ console.log(" - Session recording and heatmaps (configurable in PostHog)");
97
+ console.log();
98
+ console.log(" PostHog is privacy-friendly, open-source, and GDPR-compatible.");
99
+ console.log(" You can use PostHog Cloud or self-host it.");
100
+ console.log();
101
+
102
+ const pkgPath = join(PROJECT_ROOT, "package.json");
103
+ if (existsSync(pkgPath)) {
104
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
105
+ console.log(` Template version: ${pkg.version ?? "unknown"}`);
106
+ console.log();
107
+ }
108
+
109
+ // -- Step 2: Hosting choice -------------------------------------------------
110
+ const hostingChoice = await select({
111
+ message: "How will you use PostHog?",
112
+ choices: [
113
+ {
114
+ name: "PostHog Cloud (US)",
115
+ value: "cloud-us",
116
+ description: "Hosted by PostHog at us.i.posthog.com — easiest setup",
117
+ },
118
+ {
119
+ name: "PostHog Cloud (EU)",
120
+ value: "cloud-eu",
121
+ description: "Hosted by PostHog at eu.i.posthog.com — EU data residency",
122
+ },
123
+ {
124
+ name: "Self-hosted",
125
+ value: "self-hosted",
126
+ description: "Your own PostHog instance — full data control",
127
+ },
128
+ ],
129
+ });
130
+
131
+ let posthogHost: string;
132
+
133
+ if (hostingChoice === "cloud-us") {
134
+ posthogHost = "https://us.i.posthog.com";
135
+ console.log();
136
+ console.log(" To get your API key:");
137
+ console.log(" 1. Sign up or log in at https://app.posthog.com");
138
+ console.log(" 2. Create a project (or select existing)");
139
+ console.log(" 3. Go to Project Settings");
140
+ console.log(" 4. Copy the Project API Key");
141
+ console.log();
142
+ } else if (hostingChoice === "cloud-eu") {
143
+ posthogHost = "https://eu.i.posthog.com";
144
+ console.log();
145
+ console.log(" To get your API key:");
146
+ console.log(" 1. Sign up or log in at https://eu.posthog.com");
147
+ console.log(" 2. Create a project (or select existing)");
148
+ console.log(" 3. Go to Project Settings");
149
+ console.log(" 4. Copy the Project API Key");
150
+ console.log();
151
+ } else {
152
+ console.log();
153
+ posthogHost = await input({
154
+ message: "PostHog instance URL (e.g., https://posthog.yourdomain.com):",
155
+ validate: (value) => {
156
+ if (!value || value.trim().length === 0) return "Host URL is required.";
157
+ if (!value.startsWith("http")) return "Should start with http:// or https://";
158
+ return true;
159
+ },
160
+ });
161
+ posthogHost = posthogHost.replace(/\/+$/, ""); // Remove trailing slashes
162
+ console.log();
163
+ }
164
+
165
+ // -- Step 3: Collect API key ------------------------------------------------
166
+ const posthogKey = await input({
167
+ message: "PostHog Project API Key:",
168
+ validate: (value) => {
169
+ if (!value || value.trim().length === 0) return "API key is required.";
170
+ if (value.startsWith("phx_")) return "This looks like a personal API key. Use the Project API key (starts with 'phc_').";
171
+ return true;
172
+ },
173
+ });
174
+
175
+ // -- Step 4: Summary --------------------------------------------------------
176
+ console.log();
177
+ console.log(" Summary of changes:");
178
+ console.log(" -------------------");
179
+ console.log(" Files to create (frontend):");
180
+ console.log(" + packages/frontend/src/lib/analytics.ts");
181
+ console.log(" + packages/frontend/src/hooks/useAnalytics.ts");
182
+ console.log(" + packages/frontend/src/components/AnalyticsProvider.tsx");
183
+ console.log();
184
+ console.log(" Files to modify:");
185
+ console.log(" ~ packages/frontend/src/App.tsx");
186
+ console.log(" ~ .env.example / .env");
187
+ console.log();
188
+ console.log(" Environment variables:");
189
+ console.log(` VITE_POSTHOG_KEY=${posthogKey.slice(0, 12)}...`);
190
+ console.log(` VITE_POSTHOG_HOST=${posthogHost}`);
191
+ console.log();
192
+
193
+ // -- Step 5: Confirm --------------------------------------------------------
194
+ const proceed = await confirm({ message: "Proceed with installation?", default: true });
195
+ if (!proceed) { console.log("\n Installation cancelled.\n"); process.exit(0); }
196
+
197
+ console.log();
198
+ console.log(" Installing...");
199
+ console.log();
200
+
201
+ // -- Step 6: Copy files and modify App.tsx ----------------------------------
202
+ console.log(" Copying frontend files...");
203
+ const frontendFiles = [
204
+ { src: "frontend/lib/analytics.ts", dest: "src/lib/analytics.ts" },
205
+ { src: "frontend/hooks/useAnalytics.ts", dest: "src/hooks/useAnalytics.ts" },
206
+ { src: "frontend/components/AnalyticsProvider.tsx", dest: "src/components/AnalyticsProvider.tsx" },
207
+ ];
208
+ for (const f of frontendFiles) {
209
+ copyFile(join(SAIL_DIR, "files", f.src), join(FRONTEND_ROOT, f.dest), f.dest);
210
+ }
211
+
212
+ console.log();
213
+ console.log(" Modifying App.tsx...");
214
+
215
+ const appPath = join(FRONTEND_ROOT, "src/App.tsx");
216
+ if (existsSync(appPath)) {
217
+ let appContent = readFileSync(appPath, "utf-8");
218
+
219
+ if (!appContent.includes("AnalyticsProvider")) {
220
+ // Add import
221
+ const importLine = 'import { AnalyticsProvider } from "./components/AnalyticsProvider.js";';
222
+ // Find the last import line and add after it
223
+ const lastImport = appContent.lastIndexOf("import ");
224
+ const importLineEnd = appContent.indexOf("\n", lastImport);
225
+ appContent =
226
+ appContent.slice(0, importLineEnd + 1) +
227
+ importLine + "\n" +
228
+ appContent.slice(importLineEnd + 1);
229
+
230
+ // Wrap <AppRouter /> with <AnalyticsProvider>
231
+ appContent = appContent.replace(
232
+ "<AppRouter />",
233
+ "<AnalyticsProvider>\n <AppRouter />\n </AnalyticsProvider>",
234
+ );
235
+
236
+ writeFileSync(appPath, appContent, "utf-8");
237
+ console.log(" Modified -> App.tsx");
238
+ } else {
239
+ console.log(" Skipped (already present) -> App.tsx");
240
+ }
241
+ } else {
242
+ console.warn(" Warning: App.tsx not found. Wrap your app with <AnalyticsProvider> manually.");
243
+ }
244
+
245
+ console.log();
246
+ console.log(" Installing dependencies...");
247
+ installDeps(manifest.dependencies.frontend, "packages/frontend");
248
+
249
+ console.log();
250
+ console.log(" Updating environment files...");
251
+ appendToEnvFiles("PostHog Analytics", {
252
+ VITE_POSTHOG_KEY: posthogKey.trim(),
253
+ VITE_POSTHOG_HOST: posthogHost,
254
+ });
255
+
256
+ // -- Step 7: Next steps ------------------------------------------------------
257
+ console.log();
258
+ console.log("------------------------------------------------------------");
259
+ console.log(" PostHog Analytics installed successfully!");
260
+ console.log("------------------------------------------------------------");
261
+ console.log();
262
+ console.log(" Next steps:");
263
+ console.log();
264
+ console.log(" 1. Start your dev server:");
265
+ console.log(" npm run dev");
266
+ console.log();
267
+ console.log(" 2. Verify analytics is working:");
268
+ console.log(" - Open your app in the browser");
269
+ console.log(" - Log in and navigate between pages");
270
+ console.log(" - Check PostHog dashboard for events");
271
+ console.log();
272
+ console.log(" 3. Configure PostHog features (optional):");
273
+ console.log(" - Session Recording: enable in PostHog project settings");
274
+ console.log(" - Feature Flags: use posthog.isFeatureEnabled('flag-name')");
275
+ console.log(" - Surveys: create in-app surveys from the PostHog dashboard");
276
+ console.log();
277
+ console.log(" 4. Track custom events in your components:");
278
+ console.log();
279
+ console.log(" import { useAnalytics } from '@/hooks/useAnalytics';");
280
+ console.log();
281
+ console.log(" function MyComponent() {");
282
+ console.log(" const { trackEvent } = useAnalytics();");
283
+ console.log(" return (");
284
+ console.log(" <button onClick={() => trackEvent('button_clicked', { label: 'cta' })}>");
285
+ console.log(" Click me");
286
+ console.log(" </button>");
287
+ console.log(" );");
288
+ console.log(" }");
289
+ console.log();
290
+ console.log(" GDPR note:");
291
+ console.log(" PostHog supports cookie-less tracking and consent management.");
292
+ console.log(" If you have the GDPR sail installed, consider integrating");
293
+ console.log(" analytics consent with your consent tracking system.");
294
+ console.log();
295
+ }
296
+
297
+ main().catch((err) => { console.error("Installation failed:", err); process.exit(1); });
@@ -1,30 +1,30 @@
1
- {
2
- "name": "file-uploads",
3
- "displayName": "File Uploads",
4
- "description": "Generic file upload system with S3-compatible storage (R2, S3, MinIO). Includes file management API, upload hooks, and a file browser component.",
5
- "version": "1.0.0",
6
- "compatibility": ">=1.0.0",
7
- "requiredEnvVars": [
8
- { "key": "S3_ENDPOINT", "description": "S3-compatible endpoint URL" },
9
- { "key": "S3_ACCESS_KEY_ID", "description": "S3 access key ID" },
10
- { "key": "S3_SECRET_ACCESS_KEY", "description": "S3 secret access key" },
11
- { "key": "S3_BUCKET_NAME", "description": "S3 bucket name" },
12
- { "key": "S3_PUBLIC_URL", "description": "Public URL for serving files" },
13
- { "key": "S3_REGION", "description": "S3 region (defaults to auto)" }
14
- ],
15
- "dependencies": {
16
- "backend": {
17
- "@aws-sdk/client-s3": "^3.700.0",
18
- "@aws-sdk/s3-request-presigner": "^3.700.0"
19
- },
20
- "frontend": {}
21
- },
22
- "modifies": {
23
- "backend": ["src/index.ts", "src/db/schema/index.ts", "src/env.ts"],
24
- "frontend": ["src/router.tsx"]
25
- },
26
- "adds": {
27
- "backend": ["src/services/file-storage.ts", "src/routes/files.ts", "src/db/schema/files.ts"],
28
- "frontend": ["src/hooks/useFileUpload.ts", "src/hooks/useFiles.ts", "src/components/files/FileUploadButton.tsx", "src/components/files/FileList.tsx", "src/pages/Files.tsx"]
29
- }
30
- }
1
+ {
2
+ "name": "file-uploads",
3
+ "displayName": "File Uploads",
4
+ "description": "Generic file upload system with S3-compatible storage (R2, S3, MinIO). Includes file management API, upload hooks, and a file browser component.",
5
+ "version": "1.0.0",
6
+ "compatibility": ">=1.0.0",
7
+ "requiredEnvVars": [
8
+ { "key": "S3_ENDPOINT", "description": "S3-compatible endpoint URL" },
9
+ { "key": "S3_ACCESS_KEY_ID", "description": "S3 access key ID" },
10
+ { "key": "S3_SECRET_ACCESS_KEY", "description": "S3 secret access key" },
11
+ { "key": "S3_BUCKET_NAME", "description": "S3 bucket name" },
12
+ { "key": "S3_PUBLIC_URL", "description": "Public URL for serving files" },
13
+ { "key": "S3_REGION", "description": "S3 region (defaults to auto)" }
14
+ ],
15
+ "dependencies": {
16
+ "backend": {
17
+ "@aws-sdk/client-s3": "^3.700.0",
18
+ "@aws-sdk/s3-request-presigner": "^3.700.0"
19
+ },
20
+ "frontend": {}
21
+ },
22
+ "modifies": {
23
+ "backend": ["src/index.ts", "src/db/schema/index.ts", "src/env.ts"],
24
+ "frontend": ["src/router.tsx"]
25
+ },
26
+ "adds": {
27
+ "backend": ["src/services/file-storage.ts", "src/routes/files.ts", "src/db/schema/files.ts"],
28
+ "frontend": ["src/hooks/useFileUpload.ts", "src/hooks/useFiles.ts", "src/components/files/FileUploadButton.tsx", "src/components/files/FileList.tsx", "src/pages/Files.tsx"]
29
+ }
30
+ }