@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.
- package/dist/__tests__/sail-installer.test.js +25 -25
- package/dist/sail-installer.js +174 -174
- package/dist/scaffold.js +68 -68
- package/package.json +58 -58
- package/sails/_template/addon.json +20 -20
- package/sails/_template/install.ts +402 -402
- package/sails/admin-dashboard/README.md +117 -117
- package/sails/admin-dashboard/addon.json +28 -28
- package/sails/admin-dashboard/files/backend/middleware/admin.ts +34 -34
- package/sails/admin-dashboard/files/backend/routes/admin.ts +243 -243
- package/sails/admin-dashboard/files/frontend/components/admin/StatsCard.tsx +40 -40
- package/sails/admin-dashboard/files/frontend/components/admin/UsersTable.tsx +240 -240
- package/sails/admin-dashboard/files/frontend/hooks/useAdmin.ts +149 -149
- package/sails/admin-dashboard/files/frontend/pages/admin/Dashboard.tsx +173 -173
- package/sails/admin-dashboard/files/frontend/pages/admin/UserDetail.tsx +203 -203
- package/sails/admin-dashboard/install.ts +305 -305
- package/sails/analytics/README.md +178 -178
- package/sails/analytics/addon.json +27 -27
- package/sails/analytics/files/frontend/components/AnalyticsProvider.tsx +58 -58
- package/sails/analytics/files/frontend/hooks/useAnalytics.ts +64 -64
- package/sails/analytics/files/frontend/lib/analytics.ts +103 -103
- package/sails/analytics/install.ts +297 -297
- package/sails/file-uploads/addon.json +30 -30
- package/sails/file-uploads/files/backend/routes/files.ts +198 -198
- package/sails/file-uploads/files/backend/schema/files.ts +36 -36
- package/sails/file-uploads/files/backend/services/file-storage.ts +128 -128
- package/sails/file-uploads/files/frontend/components/FileList.tsx +248 -248
- package/sails/file-uploads/files/frontend/components/FileUploadButton.tsx +147 -147
- package/sails/file-uploads/files/frontend/hooks/useFileUpload.ts +106 -106
- package/sails/file-uploads/files/frontend/hooks/useFiles.ts +118 -118
- package/sails/file-uploads/files/frontend/pages/Files.tsx +37 -37
- package/sails/file-uploads/install.ts +466 -466
- package/sails/gdpr/README.md +174 -174
- package/sails/gdpr/addon.json +27 -27
- package/sails/gdpr/files/backend/routes/gdpr.ts +140 -140
- package/sails/gdpr/files/backend/services/gdpr.ts +293 -293
- package/sails/gdpr/files/frontend/components/auth/ConsentCheckboxes.tsx +97 -97
- package/sails/gdpr/files/frontend/components/gdpr/AccountDeletionRequest.tsx +192 -192
- package/sails/gdpr/files/frontend/components/gdpr/DataExportButton.tsx +75 -75
- package/sails/gdpr/files/frontend/pages/PrivacyPolicy.tsx +186 -186
- package/sails/gdpr/install.ts +756 -756
- package/sails/google-oauth/README.md +121 -121
- package/sails/google-oauth/addon.json +22 -22
- package/sails/google-oauth/files/GoogleButton.tsx +50 -50
- package/sails/google-oauth/install.ts +252 -252
- package/sails/i18n/README.md +193 -193
- package/sails/i18n/addon.json +30 -30
- package/sails/i18n/files/frontend/components/LanguageSwitcher.tsx +108 -108
- package/sails/i18n/files/frontend/hooks/useLanguage.ts +31 -31
- package/sails/i18n/files/frontend/lib/i18n.ts +32 -32
- package/sails/i18n/files/frontend/locales/de/common.json +44 -44
- package/sails/i18n/files/frontend/locales/en/common.json +44 -44
- package/sails/i18n/install.ts +407 -407
- package/sails/push-notifications/README.md +163 -163
- package/sails/push-notifications/addon.json +31 -31
- package/sails/push-notifications/files/backend/routes/notifications.ts +153 -153
- package/sails/push-notifications/files/backend/schema/notifications.ts +31 -31
- package/sails/push-notifications/files/backend/services/notifications.ts +117 -117
- package/sails/push-notifications/files/frontend/components/PushNotificationInit.tsx +12 -12
- package/sails/push-notifications/files/frontend/hooks/usePushNotifications.ts +154 -154
- package/sails/push-notifications/install.ts +384 -384
- package/sails/r2-storage/addon.json +29 -29
- package/sails/r2-storage/files/backend/services/storage.ts +71 -71
- package/sails/r2-storage/files/frontend/components/ProfilePictureUpload.tsx +167 -167
- package/sails/r2-storage/install.ts +412 -412
- package/sails/rate-limiting/addon.json +20 -20
- package/sails/rate-limiting/files/backend/middleware/rate-limit-store.ts +104 -104
- package/sails/rate-limiting/files/backend/middleware/rate-limit.ts +137 -137
- package/sails/rate-limiting/install.ts +300 -300
- package/sails/registry.json +107 -107
- package/sails/stripe/README.md +214 -214
- package/sails/stripe/addon.json +24 -24
- package/sails/stripe/files/backend/routes/stripe.ts +154 -154
- package/sails/stripe/files/backend/schema/stripe.ts +74 -74
- package/sails/stripe/files/backend/services/stripe.ts +224 -224
- package/sails/stripe/files/frontend/components/SubscriptionStatus.tsx +135 -135
- package/sails/stripe/files/frontend/hooks/useSubscription.ts +86 -86
- package/sails/stripe/files/frontend/pages/Checkout.tsx +116 -116
- package/sails/stripe/files/frontend/pages/Pricing.tsx +226 -226
- 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
|
+
}
|