create-pulsekit 0.0.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.
Files changed (2) hide show
  1. package/dist/index.js +381 -0
  2. package/package.json +21 -0
package/dist/index.js ADDED
@@ -0,0 +1,381 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
18
+ // If the importer is in node compatibility mode or this is not an ESM
19
+ // file that has been converted to a CommonJS file using a Babel-
20
+ // compatible transform (i.e. "__esModule" has not been set), then set
21
+ // "default" to the CommonJS "module.exports" for node compatibility.
22
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
23
+ mod
24
+ ));
25
+
26
+ // src/detect.ts
27
+ var import_node_fs = __toESM(require("fs"));
28
+ var import_node_path = __toESM(require("path"));
29
+ function detectPackageManager() {
30
+ const cwd = process.cwd();
31
+ if (import_node_fs.default.existsSync(import_node_path.default.join(cwd, "bun.lock")) || import_node_fs.default.existsSync(import_node_path.default.join(cwd, "bun.lockb"))) {
32
+ return "bun";
33
+ }
34
+ if (import_node_fs.default.existsSync(import_node_path.default.join(cwd, "pnpm-lock.yaml"))) {
35
+ return "pnpm";
36
+ }
37
+ if (import_node_fs.default.existsSync(import_node_path.default.join(cwd, "yarn.lock"))) {
38
+ return "yarn";
39
+ }
40
+ return "npm";
41
+ }
42
+ function validateNextJsProject() {
43
+ const cwd = process.cwd();
44
+ const pkgPath = import_node_path.default.join(cwd, "package.json");
45
+ if (!import_node_fs.default.existsSync(pkgPath)) {
46
+ throw new Error(
47
+ "No package.json found. Run this command from the root of a Next.js project."
48
+ );
49
+ }
50
+ const pkg = JSON.parse(import_node_fs.default.readFileSync(pkgPath, "utf8"));
51
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
52
+ if (!allDeps["next"]) {
53
+ throw new Error(
54
+ "next is not listed in package.json. This command requires a Next.js project."
55
+ );
56
+ }
57
+ if (!import_node_fs.default.existsSync(getAppDir())) {
58
+ throw new Error(
59
+ "No app/ directory found. PulseKit requires the Next.js App Router."
60
+ );
61
+ }
62
+ }
63
+ function getAppDir() {
64
+ const cwd = process.cwd();
65
+ const srcAppDir = import_node_path.default.join(cwd, "src", "app");
66
+ if (import_node_fs.default.existsSync(srcAppDir)) {
67
+ return srcAppDir;
68
+ }
69
+ return import_node_path.default.join(cwd, "app");
70
+ }
71
+
72
+ // src/prompts.ts
73
+ var import_promises = __toESM(require("readline/promises"));
74
+ var import_node_process = require("process");
75
+ var import_node_fs2 = __toESM(require("fs"));
76
+ var import_node_path2 = __toESM(require("path"));
77
+ function readEnvFile() {
78
+ const envPath = import_node_path2.default.join(process.cwd(), ".env.local");
79
+ if (!import_node_fs2.default.existsSync(envPath)) return {};
80
+ const content = import_node_fs2.default.readFileSync(envPath, "utf8");
81
+ const env = {};
82
+ for (const line of content.split("\n")) {
83
+ const trimmed = line.trim();
84
+ if (!trimmed || trimmed.startsWith("#")) continue;
85
+ const eqIndex = trimmed.indexOf("=");
86
+ if (eqIndex === -1) continue;
87
+ env[trimmed.slice(0, eqIndex).trim()] = trimmed.slice(eqIndex + 1).trim();
88
+ }
89
+ return env;
90
+ }
91
+ async function promptForConfig() {
92
+ const env = readEnvFile();
93
+ const rl = import_promises.default.createInterface({ input: import_node_process.stdin, output: import_node_process.stdout });
94
+ try {
95
+ const detectedUrl = env["NEXT_PUBLIC_SUPABASE_URL"] || "";
96
+ let supabaseUrl;
97
+ if (detectedUrl) {
98
+ const answer = await rl.question(` Supabase URL [${detectedUrl}]: `);
99
+ supabaseUrl = answer.trim() || detectedUrl;
100
+ } else {
101
+ supabaseUrl = (await rl.question(" Supabase URL: ")).trim();
102
+ if (!supabaseUrl) throw new Error("Supabase URL is required.");
103
+ }
104
+ const detectedKey = env["NEXT_PUBLIC_SUPABASE_ANON_KEY"] || env["NEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY"] || "";
105
+ let supabaseAnonKey;
106
+ if (detectedKey) {
107
+ const masked = detectedKey.slice(0, 10) + "..." + detectedKey.slice(-4);
108
+ const answer = await rl.question(` Supabase anon key [${masked}]: `);
109
+ supabaseAnonKey = answer.trim() || detectedKey;
110
+ } else {
111
+ supabaseAnonKey = (await rl.question(" Supabase anon key: ")).trim();
112
+ if (!supabaseAnonKey) throw new Error("Supabase anon key is required.");
113
+ }
114
+ const detectedDb = env["DATABASE_URL"] || "";
115
+ let databaseUrl;
116
+ if (detectedDb) {
117
+ const masked = detectedDb.slice(0, 15) + "..." + detectedDb.slice(-10);
118
+ const answer = await rl.question(` DATABASE_URL [${masked}]: `);
119
+ databaseUrl = answer.trim() || detectedDb;
120
+ } else {
121
+ databaseUrl = (await rl.question(" DATABASE_URL (postgres://...): ")).trim();
122
+ if (!databaseUrl)
123
+ throw new Error("DATABASE_URL is required for migrations.");
124
+ }
125
+ const siteIdAnswer = await rl.question(" Site ID [default]: ");
126
+ const siteId = siteIdAnswer.trim() || "default";
127
+ return { supabaseUrl, supabaseAnonKey, databaseUrl, siteId };
128
+ } finally {
129
+ rl.close();
130
+ }
131
+ }
132
+
133
+ // src/install.ts
134
+ var import_node_child_process = require("child_process");
135
+ var PACKAGES = [
136
+ "@pulsekit/core",
137
+ "@pulsekit/next",
138
+ "@pulsekit/react",
139
+ "@supabase/supabase-js"
140
+ ];
141
+ async function installPackages(pm) {
142
+ console.log(" Installing packages...\n");
143
+ const commands = {
144
+ npm: `npm install ${PACKAGES.join(" ")}`,
145
+ pnpm: `pnpm add ${PACKAGES.join(" ")}`,
146
+ yarn: `yarn add ${PACKAGES.join(" ")}`,
147
+ bun: `bun add ${PACKAGES.join(" ")}`
148
+ };
149
+ const cmd = commands[pm];
150
+ console.log(` > ${cmd}
151
+ `);
152
+ try {
153
+ (0, import_node_child_process.execSync)(cmd, { cwd: process.cwd(), stdio: "inherit" });
154
+ } catch {
155
+ throw new Error(
156
+ `Package installation failed. Try running manually:
157
+ ${cmd}`
158
+ );
159
+ }
160
+ }
161
+
162
+ // src/migrate.ts
163
+ var import_postgres = __toESM(require("postgres"));
164
+ var SQL_MAP = JSON.parse(
165
+ `{"001_init_pulse.sql":"create schema if not exists analytics;\\n\\n-- Add analytics to the schemas exposed by PostgREST\\nalter role authenticator set pgrst.db_schemas = 'public, graphql_public, analytics';\\n\\n-- Schema-level access\\ngrant usage on schema analytics to anon, authenticated, service_role;\\nalter default privileges in schema analytics grant all on tables to anon, authenticated, service_role;\\n\\ncreate table if not exists analytics.pulse_events (\\n id bigserial primary key,\\n site_id text not null,\\n session_id text,\\n path text not null,\\n event_type text not null,\\n meta jsonb,\\n created_at timestamptz not null default now()\\n);\\n\\ncreate index if not exists idx_pulse_events_site_created_at\\n on analytics.pulse_events (site_id, created_at);\\n\\ncreate index if not exists idx_pulse_events_site_path_created_at\\n on analytics.pulse_events (site_id, path, created_at);\\n\\nalter table analytics.pulse_events enable row level security; \\n \\n-- Allow the anon key (API route) to insert events\\ndrop policy if exists \\"Allow anon insert on pulse_events\\" on analytics.pulse_events;\\ncreate policy \\"Allow anon insert on pulse_events\\"\\n on analytics.pulse_events\\n for insert\\n to anon\\n with check (true);\\n\\n-- Only authenticated users (dashboard) can read events\\ndrop policy if exists \\"Allow authenticated select on pulse_events\\" on analytics.pulse_events;\\ncreate policy \\"Allow authenticated select on pulse_events\\"\\n on analytics.pulse_events\\n for select\\n to authenticated\\n using (true);\\n\\ncreate table if not exists analytics.pulse_aggregates (\\n date date not null,\\n site_id text not null,\\n path text not null,\\n total_views integer not null default 0,\\n unique_visitors integer not null default 0,\\n primary key (date, site_id, path)\\n);\\n\\n-- Grant table-level access (must be after table creation)\\ngrant all on all tables in schema analytics to anon, authenticated, service_role;\\ngrant all on all sequences in schema analytics to anon, authenticated, service_role;\\n\\nalter table analytics.pulse_aggregates enable row level security;\\n\\n-- Allow reading aggregates (dashboard)\\ndrop policy if exists \\"Allow authenticated select on pulse_aggregates\\" on analytics.pulse_aggregates;\\ncreate policy \\"Allow authenticated select on pulse_aggregates\\"\\n on analytics.pulse_aggregates\\n for select\\n to authenticated\\n using (true);\\n\\ndrop policy if exists \\"Allow anon select on pulse_aggregates\\" on analytics.pulse_aggregates;\\ncreate policy \\"Allow anon select on pulse_aggregates\\"\\n on analytics.pulse_aggregates\\n for select\\n to anon\\n using (true);\\n\\n-- Reload PostgREST config and schema cache (must be last)\\nnotify pgrst, 'reload config';\\nnotify pgrst, 'reload schema';\\n","002_aggregation_function.sql":"-- Aggregation function: rolls up raw events into daily aggregates\\ncreate or replace function analytics.pulse_refresh_aggregates(days_back integer default 7)\\nreturns void\\nlanguage sql\\nsecurity definer\\nas $$\\n insert into analytics.pulse_aggregates (date, site_id, path, total_views, unique_visitors)\\n select\\n date_trunc('day', created_at)::date as date,\\n site_id,\\n path,\\n count(*) as total_views,\\n count(distinct session_id) as unique_visitors\\n from analytics.pulse_events\\n where created_at >= now() - (days_back || ' days')::interval\\n group by 1, 2, 3\\n on conflict (date, site_id, path) do update\\n set\\n total_views = excluded.total_views,\\n unique_visitors = excluded.unique_visitors;\\n$$;\\n\\n-- Allow all roles to execute the aggregation function\\n-- security definer ensures it runs with the owner's privileges regardless of caller\\ngrant execute on function analytics.pulse_refresh_aggregates(integer) to anon, authenticated, service_role;\\n","003_geo_and_timezone.sql":"-- Add geo columns to pulse_events\\nalter table analytics.pulse_events\\n add column if not exists country text,\\n add column if not exists region text,\\n add column if not exists city text,\\n add column if not exists timezone text,\\n add column if not exists latitude double precision,\\n add column if not exists longitude double precision;\\n\\n-- Timezone-aware stats: queries raw events with AT TIME ZONE\\n-- so the dashboard can display data bucketed by the viewer's local day.\\ncreate or replace function analytics.pulse_stats_by_timezone(\\n p_site_id text,\\n p_timezone text default 'UTC',\\n p_days_back integer default 7\\n)\\nreturns table (\\n date date,\\n path text,\\n total_views bigint,\\n unique_visitors bigint\\n)\\nlanguage sql\\nsecurity definer\\nstable\\nas $$\\n select\\n date_trunc('day', created_at at time zone p_timezone)::date as date,\\n path,\\n count(*) as total_views,\\n count(distinct session_id) as unique_visitors\\n from analytics.pulse_events\\n where site_id = p_site_id\\n and created_at >= now() - make_interval(days => p_days_back + 1)\\n group by 1, 2;\\n$$;\\n\\ngrant execute on function analytics.pulse_stats_by_timezone(text, text, integer)\\n to anon, authenticated, service_role;\\n\\n-- Drop first so return type can change (CREATE OR REPLACE cannot alter return columns)\\ndrop function if exists analytics.pulse_location_stats(text, integer);\\n\\n-- Location stats: visitor counts grouped by country + city, with averaged coordinates\\ncreate or replace function analytics.pulse_location_stats(\\n p_site_id text,\\n p_days_back integer default 7\\n)\\nreturns table (\\n country text,\\n city text,\\n latitude double precision,\\n longitude double precision,\\n total_views bigint,\\n unique_visitors bigint\\n)\\nlanguage sql\\nsecurity definer\\nstable\\nas $$\\n select\\n country,\\n city,\\n avg(latitude) as latitude,\\n avg(longitude) as longitude,\\n count(*) as total_views,\\n count(distinct session_id) as unique_visitors\\n from analytics.pulse_events\\n where site_id = p_site_id\\n and created_at >= now() - make_interval(days => p_days_back)\\n and country is not null\\n group by 1, 2\\n order by total_views desc;\\n$$;\\n\\ngrant execute on function analytics.pulse_location_stats(text, integer)\\n to anon, authenticated, service_role;\\n","004_web_vitals.sql":"-- 004_web_vitals.sql\\n-- Partial index + RPC for Web Vitals p75 aggregation\\n\\n-- Partial index: only covers vitals events, stays small\\nCREATE INDEX IF NOT EXISTS idx_pulse_events_vitals\\n ON analytics.pulse_events (site_id, created_at)\\n WHERE event_type = 'vitals';\\n\\n-- RPC: returns per-metric p75 for each page + site-wide (__overall__)\\nCREATE OR REPLACE FUNCTION analytics.pulse_vitals_stats(\\n p_site_id TEXT,\\n p_days_back INT DEFAULT 7\\n)\\nRETURNS TABLE (\\n path TEXT,\\n metric TEXT,\\n p75 DOUBLE PRECISION,\\n sample_count BIGINT\\n)\\nLANGUAGE sql SECURITY DEFINER STABLE\\nAS $$\\n WITH vitals_raw AS (\\n SELECT\\n e.path,\\n kv.key AS metric,\\n kv.value::double precision AS val\\n FROM analytics.pulse_events e,\\n LATERAL jsonb_each_text(e.meta) AS kv(key, value)\\n WHERE e.site_id = p_site_id\\n AND e.event_type = 'vitals'\\n AND e.created_at >= NOW() - (p_days_back || ' days')::interval\\n AND kv.key IN ('lcp', 'inp', 'cls', 'fcp', 'ttfb')\\n )\\n -- Per-page stats\\n SELECT\\n vr.path,\\n vr.metric,\\n percentile_cont(0.75) WITHIN GROUP (ORDER BY vr.val) AS p75,\\n count(*)::bigint AS sample_count\\n FROM vitals_raw vr\\n GROUP BY vr.path, vr.metric\\n\\n UNION ALL\\n\\n -- Site-wide stats\\n SELECT\\n '__overall__'::text AS path,\\n vr.metric,\\n percentile_cont(0.75) WITHIN GROUP (ORDER BY vr.val) AS p75,\\n count(*)::bigint AS sample_count\\n FROM vitals_raw vr\\n GROUP BY vr.metric;\\n$$;\\n\\nGRANT EXECUTE ON FUNCTION analytics.pulse_vitals_stats(TEXT, INT)\\n TO anon, authenticated, service_role;\\n"}`
166
+ );
167
+ async function runMigrations(databaseUrl) {
168
+ console.log(" Running SQL migrations...\n");
169
+ const sql = (0, import_postgres.default)(databaseUrl, { max: 1 });
170
+ const files = Object.keys(SQL_MAP).sort();
171
+ for (const file of files) {
172
+ try {
173
+ await sql.unsafe(SQL_MAP[file]);
174
+ console.log(` Applied: ${file}`);
175
+ } catch (err) {
176
+ const msg = err?.message || "";
177
+ if (msg.includes("already exists") || msg.includes("duplicate key")) {
178
+ console.log(` Skipped (already applied): ${file}`);
179
+ } else {
180
+ await sql.end();
181
+ throw new Error(`Migration ${file} failed: ${msg}`);
182
+ }
183
+ }
184
+ }
185
+ await sql.end();
186
+ console.log("\n Migrations complete.\n");
187
+ }
188
+
189
+ // src/scaffold.ts
190
+ var import_node_fs3 = __toESM(require("fs"));
191
+ var import_node_path3 = __toESM(require("path"));
192
+ async function scaffoldFiles(siteId) {
193
+ console.log(" Scaffolding files...\n");
194
+ const appDir = getAppDir();
195
+ const pulseRoute = `import { createPulseHandler } from "@pulsekit/next";
196
+ import { createClient } from "@supabase/supabase-js";
197
+
198
+ const supabase = createClient(
199
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
200
+ process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
201
+ );
202
+
203
+ export const POST = createPulseHandler({
204
+ supabase,
205
+ config: {
206
+ allowLocalhost: true,
207
+ siteId: ${JSON.stringify(siteId)},
208
+ },
209
+ });
210
+ `;
211
+ const refreshRoute = `import { createRefreshHandler } from "@pulsekit/next";
212
+ import { createClient } from "@supabase/supabase-js";
213
+
214
+ const supabase = createClient(
215
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
216
+ process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
217
+ );
218
+
219
+ export const POST = createRefreshHandler({ supabase });
220
+ `;
221
+ const dashboardPage = `import { createClient } from "@supabase/supabase-js";
222
+ import { PulseDashboard } from "@pulsekit/react";
223
+ import { getPulseTimezone } from "@pulsekit/next";
224
+
225
+ const supabase = createClient(
226
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
227
+ process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
228
+ );
229
+
230
+ export default async function AnalyticsPage() {
231
+ const timezone = await getPulseTimezone();
232
+
233
+ return (
234
+ <PulseDashboard
235
+ supabase={supabase}
236
+ siteId={${JSON.stringify(siteId)}}
237
+ timeframe="7d"
238
+ timezone={timezone}
239
+ />
240
+ );
241
+ }
242
+ `;
243
+ const files = [
244
+ { rel: "api/pulse/route.ts", content: pulseRoute },
245
+ { rel: "api/pulse/refresh-aggregates/route.ts", content: refreshRoute },
246
+ { rel: "admin/analytics/page.tsx", content: dashboardPage }
247
+ ];
248
+ for (const { rel, content } of files) {
249
+ const fullPath = import_node_path3.default.join(appDir, rel);
250
+ import_node_fs3.default.mkdirSync(import_node_path3.default.dirname(fullPath), { recursive: true });
251
+ if (import_node_fs3.default.existsSync(fullPath)) {
252
+ console.log(` Skipped (already exists): ${rel}`);
253
+ continue;
254
+ }
255
+ import_node_fs3.default.writeFileSync(fullPath, content, "utf8");
256
+ console.log(` Created: ${rel}`);
257
+ }
258
+ console.log("");
259
+ }
260
+
261
+ // src/inject.ts
262
+ var import_node_fs4 = __toESM(require("fs"));
263
+ var import_node_path4 = __toESM(require("path"));
264
+ async function injectPulseTracker() {
265
+ console.log(" Injecting PulseTracker into layout...\n");
266
+ const appDir = getAppDir();
267
+ const candidates = [
268
+ import_node_path4.default.join(appDir, "layout.tsx"),
269
+ import_node_path4.default.join(appDir, "layout.jsx"),
270
+ import_node_path4.default.join(appDir, "layout.js")
271
+ ];
272
+ let foundPath = null;
273
+ for (const candidate of candidates) {
274
+ if (import_node_fs4.default.existsSync(candidate)) {
275
+ foundPath = candidate;
276
+ break;
277
+ }
278
+ }
279
+ if (!foundPath) {
280
+ printManualInstructions();
281
+ return;
282
+ }
283
+ let content = import_node_fs4.default.readFileSync(foundPath, "utf8");
284
+ if (content.includes("PulseTracker")) {
285
+ console.log(" PulseTracker already present in layout. Skipping.\n");
286
+ return;
287
+ }
288
+ const importStatement = 'import { PulseTracker } from "@pulsekit/next/client";';
289
+ const importRegex = /^import\s.+$/gm;
290
+ let lastImportEnd = -1;
291
+ let match;
292
+ while ((match = importRegex.exec(content)) !== null) {
293
+ lastImportEnd = match.index + match[0].length;
294
+ }
295
+ if (lastImportEnd === -1) {
296
+ content = importStatement + "\n" + content;
297
+ } else {
298
+ content = content.slice(0, lastImportEnd) + "\n" + importStatement + content.slice(lastImportEnd);
299
+ }
300
+ const bodyCloseIndex = content.lastIndexOf("</body>");
301
+ if (bodyCloseIndex === -1) {
302
+ printManualInstructions();
303
+ return;
304
+ }
305
+ const lineStart = content.lastIndexOf("\n", bodyCloseIndex) + 1;
306
+ const indent = content.slice(lineStart, bodyCloseIndex).match(/^\s*/)?.[0] ?? " ";
307
+ const trackerJsx = `${indent} <PulseTracker excludePaths={["/admin/analytics"]} />
308
+ `;
309
+ content = content.slice(0, bodyCloseIndex) + trackerJsx + content.slice(bodyCloseIndex);
310
+ import_node_fs4.default.writeFileSync(foundPath, content, "utf8");
311
+ console.log(
312
+ ` Modified: ${import_node_path4.default.relative(process.cwd(), foundPath)}
313
+ `
314
+ );
315
+ }
316
+ function printManualInstructions() {
317
+ console.log(
318
+ " Could not auto-inject PulseTracker. Add it manually to your layout:\n"
319
+ );
320
+ console.log(' import { PulseTracker } from "@pulsekit/next/client";');
321
+ console.log("");
322
+ console.log(" // Add inside your <body> tag:");
323
+ console.log(
324
+ ' <PulseTracker excludePaths={["/admin/analytics"]} />\n'
325
+ );
326
+ }
327
+
328
+ // src/env.ts
329
+ var import_node_fs5 = __toESM(require("fs"));
330
+ var import_node_path5 = __toESM(require("path"));
331
+ function writeEnvVars(config) {
332
+ console.log(" Updating .env.local...\n");
333
+ const envPath = import_node_path5.default.join(process.cwd(), ".env.local");
334
+ let existing = "";
335
+ if (import_node_fs5.default.existsSync(envPath)) {
336
+ existing = import_node_fs5.default.readFileSync(envPath, "utf8");
337
+ }
338
+ const lines = [];
339
+ if (!existing.includes("NEXT_PUBLIC_SUPABASE_URL")) {
340
+ lines.push(`NEXT_PUBLIC_SUPABASE_URL=${config.supabaseUrl}`);
341
+ }
342
+ if (!existing.includes("NEXT_PUBLIC_SUPABASE_ANON_KEY") && !existing.includes("NEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY")) {
343
+ lines.push(`NEXT_PUBLIC_SUPABASE_ANON_KEY=${config.supabaseAnonKey}`);
344
+ }
345
+ if (!existing.includes("DATABASE_URL")) {
346
+ lines.push(`DATABASE_URL=${config.databaseUrl}`);
347
+ }
348
+ if (lines.length === 0) {
349
+ console.log(" .env.local already has all required variables.\n");
350
+ return;
351
+ }
352
+ const separator = existing.endsWith("\n") || existing === "" ? "" : "\n";
353
+ const addition = separator + "\n# PulseKit Analytics\n" + lines.join("\n") + "\n";
354
+ import_node_fs5.default.appendFileSync(envPath, addition, "utf8");
355
+ console.log(" Updated .env.local with PulseKit variables.\n");
356
+ }
357
+
358
+ // src/index.ts
359
+ async function main() {
360
+ console.log("\n create-pulsekit\n");
361
+ console.log(" Setting up PulseKit analytics in your Next.js project.\n");
362
+ const pm = detectPackageManager();
363
+ console.log(` Detected package manager: ${pm}
364
+ `);
365
+ validateNextJsProject();
366
+ const config = await promptForConfig();
367
+ writeEnvVars(config);
368
+ await installPackages(pm);
369
+ await runMigrations(config.databaseUrl);
370
+ await scaffoldFiles(config.siteId);
371
+ await injectPulseTracker();
372
+ console.log("\n Done! PulseKit analytics is ready.\n");
373
+ console.log(" Next steps:");
374
+ console.log(" 1. Start your dev server");
375
+ console.log(" 2. Visit any page to generate pageview events");
376
+ console.log(" 3. Go to /admin/analytics to see your dashboard");
377
+ }
378
+ main().catch((err) => {
379
+ console.error("\n Error:", err.message || err);
380
+ process.exit(1);
381
+ });
package/package.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "create-pulsekit",
3
+ "version": "0.0.1",
4
+ "description": "Set up PulseKit analytics in your Next.js project",
5
+ "bin": "./dist/index.js",
6
+ "files": [
7
+ "dist"
8
+ ],
9
+ "dependencies": {
10
+ "postgres": "^3.4.0"
11
+ },
12
+ "devDependencies": {
13
+ "tsup": "^8.0.0",
14
+ "typescript": "^5.7.0"
15
+ },
16
+ "scripts": {
17
+ "prebuild": "cp ../core/sql/*.sql src/sql/",
18
+ "build": "tsup",
19
+ "clean": "rm -rf dist"
20
+ }
21
+ }