@gurulu/cli 0.4.7 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +92 -0
- package/README.md +35 -106
- package/dist/bin.d.ts +3 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +25410 -0
- package/dist/commands/auth.d.ts +23 -20
- package/dist/commands/auth.d.ts.map +1 -0
- package/dist/commands/doctor.d.ts +20 -6
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/init.d.ts +25 -11
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/pull.d.ts +13 -0
- package/dist/commands/pull.d.ts.map +1 -0
- package/dist/commands/push.d.ts +40 -0
- package/dist/commands/push.d.ts.map +1 -0
- package/dist/commands/validate.d.ts +36 -0
- package/dist/commands/validate.d.ts.map +1 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +24985 -876
- package/dist/lib/api.d.ts +139 -0
- package/dist/lib/api.d.ts.map +1 -0
- package/dist/lib/codegen.d.ts +4 -0
- package/dist/lib/codegen.d.ts.map +1 -0
- package/dist/lib/config.d.ts +43 -0
- package/dist/lib/config.d.ts.map +1 -0
- package/package.json +40 -20
- package/bin/gurulu.js +0 -2
- package/dist/api-client.d.ts +0 -33
- package/dist/api-client.js +0 -175
- package/dist/commands/add-server.d.ts +0 -9
- package/dist/commands/add-server.js +0 -162
- package/dist/commands/alerts.d.ts +0 -27
- package/dist/commands/alerts.js +0 -309
- package/dist/commands/api-keys.d.ts +0 -20
- package/dist/commands/api-keys.js +0 -130
- package/dist/commands/attribution.d.ts +0 -22
- package/dist/commands/attribution.js +0 -111
- package/dist/commands/audiences.d.ts +0 -23
- package/dist/commands/audiences.js +0 -243
- package/dist/commands/audit.d.ts +0 -20
- package/dist/commands/audit.js +0 -130
- package/dist/commands/auth.js +0 -249
- package/dist/commands/chat.d.ts +0 -19
- package/dist/commands/chat.js +0 -118
- package/dist/commands/config.d.ts +0 -10
- package/dist/commands/config.js +0 -92
- package/dist/commands/consent.d.ts +0 -27
- package/dist/commands/consent.js +0 -233
- package/dist/commands/conversion-paths.d.ts +0 -19
- package/dist/commands/conversion-paths.js +0 -55
- package/dist/commands/db.d.ts +0 -25
- package/dist/commands/db.js +0 -330
- package/dist/commands/destinations.d.ts +0 -20
- package/dist/commands/destinations.js +0 -191
- package/dist/commands/doctor.js +0 -360
- package/dist/commands/errors.d.ts +0 -27
- package/dist/commands/errors.js +0 -121
- package/dist/commands/events.d.ts +0 -33
- package/dist/commands/events.js +0 -371
- package/dist/commands/experiments.d.ts +0 -22
- package/dist/commands/experiments.js +0 -264
- package/dist/commands/funnels.d.ts +0 -17
- package/dist/commands/funnels.js +0 -203
- package/dist/commands/goals.d.ts +0 -18
- package/dist/commands/goals.js +0 -214
- package/dist/commands/heatmap.d.ts +0 -27
- package/dist/commands/heatmap.js +0 -112
- package/dist/commands/identity.d.ts +0 -29
- package/dist/commands/identity.js +0 -328
- package/dist/commands/init.js +0 -215
- package/dist/commands/insights.d.ts +0 -10
- package/dist/commands/insights.js +0 -77
- package/dist/commands/install.d.ts +0 -259
- package/dist/commands/install.js +0 -1590
- package/dist/commands/login.d.ts +0 -20
- package/dist/commands/login.js +0 -170
- package/dist/commands/logout.d.ts +0 -10
- package/dist/commands/logout.js +0 -41
- package/dist/commands/playground.d.ts +0 -11
- package/dist/commands/playground.js +0 -47
- package/dist/commands/releases.d.ts +0 -17
- package/dist/commands/releases.js +0 -54
- package/dist/commands/replay.d.ts +0 -18
- package/dist/commands/replay.js +0 -64
- package/dist/commands/secrets.d.ts +0 -19
- package/dist/commands/secrets.js +0 -145
- package/dist/commands/setup.d.ts +0 -21
- package/dist/commands/setup.js +0 -67
- package/dist/commands/sites.d.ts +0 -18
- package/dist/commands/sites.js +0 -139
- package/dist/commands/skad.d.ts +0 -18
- package/dist/commands/skad.js +0 -53
- package/dist/commands/sourcemap.d.ts +0 -33
- package/dist/commands/sourcemap.js +0 -204
- package/dist/commands/status.d.ts +0 -7
- package/dist/commands/status.js +0 -136
- package/dist/commands/upgrade.d.ts +0 -21
- package/dist/commands/upgrade.js +0 -183
- package/dist/commands/warehouse.d.ts +0 -20
- package/dist/commands/warehouse.js +0 -65
- package/dist/commands/warehouses.d.ts +0 -17
- package/dist/commands/warehouses.js +0 -182
- package/dist/commands/watch.d.ts +0 -45
- package/dist/commands/watch.js +0 -258
- package/dist/commands/whoami.d.ts +0 -9
- package/dist/commands/whoami.js +0 -50
- package/dist/config.d.ts +0 -75
- package/dist/config.js +0 -329
- package/dist/frameworks/detect.d.ts +0 -8
- package/dist/frameworks/detect.js +0 -458
- package/dist/install-intent-proposal.d.ts +0 -99
- package/dist/install-intent-proposal.js +0 -202
- package/dist/utils/api.d.ts +0 -20
- package/dist/utils/api.js +0 -47
- package/dist/utils/config.d.ts +0 -13
- package/dist/utils/config.js +0 -30
- package/dist/utils/confirm.d.ts +0 -17
- package/dist/utils/confirm.js +0 -40
- package/dist/utils/dry-run.d.ts +0 -20
- package/dist/utils/dry-run.js +0 -67
- package/dist/utils/from-file.d.ts +0 -9
- package/dist/utils/from-file.js +0 -72
- package/dist/utils/redact.d.ts +0 -14
- package/dist/utils/redact.js +0 -48
- package/dist/utils/ui.d.ts +0 -14
- package/dist/utils/ui.js +0 -59
- package/scripts/.gitkeep +0 -0
- package/scripts/README-gurulu-agentic-install.md +0 -114
- package/scripts/README-gurulu-scan.md +0 -98
- package/scripts/audit-cli-scopes.mjs +0 -204
- package/scripts/backfill-tenant-id.mjs +0 -172
- package/scripts/backfill-tenant-links.ts +0 -252
- package/scripts/backup-clickhouse.sh +0 -27
- package/scripts/backup-postgres.sh +0 -19
- package/scripts/bootstrap-runtime-schema.mjs +0 -87
- package/scripts/bootstrap-stripe.mjs +0 -158
- package/scripts/gurulu-agentic-install.lib.cjs +0 -762
- package/scripts/gurulu-agentic-install.mjs +0 -623
- package/scripts/gurulu-scan.lib.cjs +0 -1509
- package/scripts/gurulu-scan.mjs +0 -91
- package/scripts/gurulu-verify-install.lib.cjs +0 -334
- package/scripts/gurulu-verify-install.mjs +0 -59
- package/scripts/init-ssl.sh +0 -26
- package/scripts/migrate-flow-graph-enums.sh +0 -86
- package/scripts/monitor-disk.sh +0 -24
- package/scripts/patches/astro.patch.cjs +0 -74
- package/scripts/patches/auto-instrument/ast-helper.cjs +0 -480
- package/scripts/patches/auto-instrument/astro.cjs +0 -273
- package/scripts/patches/auto-instrument/express.cjs +0 -383
- package/scripts/patches/auto-instrument/fastify.cjs +0 -262
- package/scripts/patches/auto-instrument/hono.cjs +0 -392
- package/scripts/patches/auto-instrument/index.cjs +0 -80
- package/scripts/patches/auto-instrument/nestjs.cjs +0 -286
- package/scripts/patches/auto-instrument/nextjs-app-router.cjs +0 -345
- package/scripts/patches/auto-instrument/nextjs-pages.cjs +0 -361
- package/scripts/patches/auto-instrument/remix.cjs +0 -168
- package/scripts/patches/auto-instrument/sdk-helper-map.cjs +0 -241
- package/scripts/patches/auto-instrument/singleton-helper.cjs +0 -193
- package/scripts/patches/auto-instrument/sveltekit.cjs +0 -161
- package/scripts/patches/auto-instrument/vite-react.cjs +0 -37
- package/scripts/patches/auto-instrument/vue.cjs +0 -196
- package/scripts/patches/express.patch.cjs +0 -99
- package/scripts/patches/fastify.patch.cjs +0 -108
- package/scripts/patches/index.cjs +0 -300
- package/scripts/patches/nestjs.patch.cjs +0 -112
- package/scripts/patches/nextjs-app-router.patch.cjs +0 -97
- package/scripts/patches/nextjs-pages.patch.cjs +0 -97
- package/scripts/patches/remix.patch.cjs +0 -75
- package/scripts/patches/sveltekit.patch.cjs +0 -72
- package/scripts/patches/vite-react.patch.cjs +0 -73
- package/scripts/patches/vue.patch.cjs +0 -82
- package/scripts/renew-ssl.sh +0 -14
- package/scripts/resolve-migration.sh +0 -23
- package/scripts/seed-cli-dev-keys.mjs +0 -130
- package/scripts/seed-test-data.mjs +0 -391
- package/scripts/spike-browserless.ts +0 -65
- package/scripts/tenant-pivot-consistency-check.mjs +0 -205
- package/scripts/tenant-pivot-phase-3-cleanup.lib.cjs +0 -258
- package/scripts/tenant-pivot-phase-3-cleanup.mjs +0 -98
- package/scripts/test-identity-resolution.ts +0 -804
- package/scripts/validate-gurulu-schemas.mjs +0 -79
|
@@ -1,252 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env npx tsx
|
|
2
|
-
/**
|
|
3
|
-
* Backfill script: scan all deterministic claims in a tenant, group by
|
|
4
|
-
* (claimType, claimValue), and create TenantIdentityLink + members for
|
|
5
|
-
* any claims that exist across sites.
|
|
6
|
-
*
|
|
7
|
-
* Usage:
|
|
8
|
-
* npx tsx scripts/backfill-tenant-links.ts <tenantId>
|
|
9
|
-
* npx tsx scripts/backfill-tenant-links.ts --all
|
|
10
|
-
*
|
|
11
|
-
* Safe to run multiple times — all operations are idempotent upserts.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { PrismaClient } from '@prisma/client';
|
|
15
|
-
import { hashClaimForTenant } from '../src/lib/identity/tenant-link';
|
|
16
|
-
import { clickhouseInsert } from '../src/lib/clickhouse';
|
|
17
|
-
|
|
18
|
-
const prisma = new PrismaClient();
|
|
19
|
-
|
|
20
|
-
const DETERMINISTIC_TYPES = ['email', 'phone', 'oauth_id'];
|
|
21
|
-
const BATCH_SIZE = 500;
|
|
22
|
-
|
|
23
|
-
interface ClaimGroup {
|
|
24
|
-
claimType: string;
|
|
25
|
-
claimValue: string;
|
|
26
|
-
entries: Array<{ siteId: string; canonicalPersonId: string }>;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
async function backfillTenantLinks(tenantId: string): Promise<{
|
|
30
|
-
linksCreated: number;
|
|
31
|
-
membersCreated: number;
|
|
32
|
-
claimsScanned: number;
|
|
33
|
-
}> {
|
|
34
|
-
console.log(`[backfill] Starting tenant ${tenantId}...`);
|
|
35
|
-
|
|
36
|
-
let linksCreated = 0;
|
|
37
|
-
let membersCreated = 0;
|
|
38
|
-
let claimsScanned = 0;
|
|
39
|
-
const now = new Date();
|
|
40
|
-
|
|
41
|
-
// Fetch all deterministic claims for this tenant in batches.
|
|
42
|
-
// We join through CanonicalPerson to get tenantId filtering.
|
|
43
|
-
let cursor: string | undefined;
|
|
44
|
-
const claimMap = new Map<string, ClaimGroup>();
|
|
45
|
-
|
|
46
|
-
while (true) {
|
|
47
|
-
const claims = await prisma.identityClaim.findMany({
|
|
48
|
-
where: {
|
|
49
|
-
claimType: { in: DETERMINISTIC_TYPES },
|
|
50
|
-
retiredAt: null,
|
|
51
|
-
canonicalPerson: { tenantId },
|
|
52
|
-
},
|
|
53
|
-
select: {
|
|
54
|
-
id: true,
|
|
55
|
-
claimType: true,
|
|
56
|
-
claimValue: true,
|
|
57
|
-
siteId: true,
|
|
58
|
-
canonicalPersonId: true,
|
|
59
|
-
},
|
|
60
|
-
orderBy: { id: 'asc' },
|
|
61
|
-
take: BATCH_SIZE,
|
|
62
|
-
...(cursor ? { skip: 1, cursor: { id: cursor } } : {}),
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
if (claims.length === 0) break;
|
|
66
|
-
cursor = claims[claims.length - 1].id;
|
|
67
|
-
claimsScanned += claims.length;
|
|
68
|
-
|
|
69
|
-
for (const claim of claims) {
|
|
70
|
-
const key = `${claim.claimType}:${claim.claimValue}`;
|
|
71
|
-
let group = claimMap.get(key);
|
|
72
|
-
if (!group) {
|
|
73
|
-
group = {
|
|
74
|
-
claimType: claim.claimType,
|
|
75
|
-
claimValue: claim.claimValue,
|
|
76
|
-
entries: [],
|
|
77
|
-
};
|
|
78
|
-
claimMap.set(key, group);
|
|
79
|
-
}
|
|
80
|
-
// Deduplicate by (siteId, canonicalPersonId).
|
|
81
|
-
const exists = group.entries.some(
|
|
82
|
-
(e) => e.siteId === claim.siteId && e.canonicalPersonId === claim.canonicalPersonId,
|
|
83
|
-
);
|
|
84
|
-
if (!exists) {
|
|
85
|
-
group.entries.push({
|
|
86
|
-
siteId: claim.siteId,
|
|
87
|
-
canonicalPersonId: claim.canonicalPersonId,
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
if (claims.length < BATCH_SIZE) break;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
console.log(
|
|
96
|
-
`[backfill] Scanned ${claimsScanned} claims, found ${claimMap.size} unique deterministic claim groups`,
|
|
97
|
-
);
|
|
98
|
-
|
|
99
|
-
// Process each claim group.
|
|
100
|
-
const chRows: Array<Record<string, unknown>> = [];
|
|
101
|
-
|
|
102
|
-
for (const [, group] of claimMap) {
|
|
103
|
-
const claimValueHash = hashClaimForTenant(group.claimValue, tenantId);
|
|
104
|
-
|
|
105
|
-
// Upsert the TenantIdentityLink.
|
|
106
|
-
let link = await prisma.tenantIdentityLink.findUnique({
|
|
107
|
-
where: {
|
|
108
|
-
tenantId_claimType_claimValueHash: {
|
|
109
|
-
tenantId,
|
|
110
|
-
claimType: group.claimType,
|
|
111
|
-
claimValueHash,
|
|
112
|
-
},
|
|
113
|
-
},
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
if (!link) {
|
|
117
|
-
try {
|
|
118
|
-
link = await prisma.tenantIdentityLink.create({
|
|
119
|
-
data: {
|
|
120
|
-
tenantId,
|
|
121
|
-
claimType: group.claimType,
|
|
122
|
-
claimValueHash,
|
|
123
|
-
confidence: 1.0,
|
|
124
|
-
firstLinkedAt: now,
|
|
125
|
-
lastSeenAt: now,
|
|
126
|
-
},
|
|
127
|
-
});
|
|
128
|
-
linksCreated++;
|
|
129
|
-
} catch {
|
|
130
|
-
// Race with concurrent run — fetch it.
|
|
131
|
-
link = await prisma.tenantIdentityLink.findUnique({
|
|
132
|
-
where: {
|
|
133
|
-
tenantId_claimType_claimValueHash: {
|
|
134
|
-
tenantId,
|
|
135
|
-
claimType: group.claimType,
|
|
136
|
-
claimValueHash,
|
|
137
|
-
},
|
|
138
|
-
},
|
|
139
|
-
});
|
|
140
|
-
if (!link) continue;
|
|
141
|
-
}
|
|
142
|
-
} else {
|
|
143
|
-
await prisma.tenantIdentityLink.update({
|
|
144
|
-
where: { id: link.id },
|
|
145
|
-
data: { lastSeenAt: now },
|
|
146
|
-
});
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// Upsert members for each (site, person) pair.
|
|
150
|
-
for (const entry of group.entries) {
|
|
151
|
-
const existing = await prisma.tenantIdentityLinkMember.findUnique({
|
|
152
|
-
where: {
|
|
153
|
-
tenantIdentityLinkId_siteId_canonicalPersonId: {
|
|
154
|
-
tenantIdentityLinkId: link.id,
|
|
155
|
-
siteId: entry.siteId,
|
|
156
|
-
canonicalPersonId: entry.canonicalPersonId,
|
|
157
|
-
},
|
|
158
|
-
},
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
if (!existing) {
|
|
162
|
-
try {
|
|
163
|
-
await prisma.tenantIdentityLinkMember.create({
|
|
164
|
-
data: {
|
|
165
|
-
tenantIdentityLinkId: link.id,
|
|
166
|
-
siteId: entry.siteId,
|
|
167
|
-
canonicalPersonId: entry.canonicalPersonId,
|
|
168
|
-
firstSeenAt: now,
|
|
169
|
-
lastSeenAt: now,
|
|
170
|
-
},
|
|
171
|
-
});
|
|
172
|
-
membersCreated++;
|
|
173
|
-
} catch {
|
|
174
|
-
// Race — already exists.
|
|
175
|
-
}
|
|
176
|
-
} else {
|
|
177
|
-
await prisma.tenantIdentityLinkMember.update({
|
|
178
|
-
where: { id: existing.id },
|
|
179
|
-
data: { lastSeenAt: now },
|
|
180
|
-
});
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// Collect ClickHouse row.
|
|
184
|
-
chRows.push({
|
|
185
|
-
tenant_id: tenantId,
|
|
186
|
-
link_id: link.id,
|
|
187
|
-
claim_type: group.claimType,
|
|
188
|
-
claim_value_hash: claimValueHash,
|
|
189
|
-
site_id: entry.siteId,
|
|
190
|
-
canonical_person_id: entry.canonicalPersonId,
|
|
191
|
-
confidence: link.confidence,
|
|
192
|
-
first_linked_at: link.firstLinkedAt.toISOString(),
|
|
193
|
-
last_seen_at: now.toISOString(),
|
|
194
|
-
updated_at: now.toISOString(),
|
|
195
|
-
});
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// Sync to ClickHouse in chunks.
|
|
200
|
-
const CH_CHUNK = 100;
|
|
201
|
-
for (let i = 0; i < chRows.length; i += CH_CHUNK) {
|
|
202
|
-
const chunk = chRows.slice(i, i + CH_CHUNK);
|
|
203
|
-
try {
|
|
204
|
-
await clickhouseInsert('tenant_identity_links', chunk);
|
|
205
|
-
} catch (err) {
|
|
206
|
-
console.warn('[backfill] ClickHouse sync chunk failed:', (err as Error).message);
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
console.log(
|
|
211
|
-
`[backfill] Tenant ${tenantId} done: ${linksCreated} links created, ${membersCreated} members created, ${chRows.length} CH rows synced`,
|
|
212
|
-
);
|
|
213
|
-
|
|
214
|
-
return { linksCreated, membersCreated, claimsScanned };
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
async function main() {
|
|
218
|
-
const arg = process.argv[2];
|
|
219
|
-
if (!arg) {
|
|
220
|
-
console.error('Usage: npx tsx scripts/backfill-tenant-links.ts <tenantId> | --all');
|
|
221
|
-
process.exit(1);
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
try {
|
|
225
|
-
if (arg === '--all') {
|
|
226
|
-
const tenants = await prisma.tenant.findMany({ select: { id: true, name: true } });
|
|
227
|
-
console.log(`[backfill] Processing ${tenants.length} tenants...`);
|
|
228
|
-
let totalLinks = 0;
|
|
229
|
-
let totalMembers = 0;
|
|
230
|
-
let totalClaims = 0;
|
|
231
|
-
for (const tenant of tenants) {
|
|
232
|
-
console.log(`\n[backfill] === Tenant: ${tenant.name} (${tenant.id}) ===`);
|
|
233
|
-
const result = await backfillTenantLinks(tenant.id);
|
|
234
|
-
totalLinks += result.linksCreated;
|
|
235
|
-
totalMembers += result.membersCreated;
|
|
236
|
-
totalClaims += result.claimsScanned;
|
|
237
|
-
}
|
|
238
|
-
console.log(
|
|
239
|
-
`\n[backfill] ALL DONE: ${totalLinks} links, ${totalMembers} members across ${totalClaims} claims`,
|
|
240
|
-
);
|
|
241
|
-
} else {
|
|
242
|
-
await backfillTenantLinks(arg);
|
|
243
|
-
}
|
|
244
|
-
} finally {
|
|
245
|
-
await prisma.$disconnect();
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
main().catch((err) => {
|
|
250
|
-
console.error('[backfill] Fatal error:', err);
|
|
251
|
-
process.exit(1);
|
|
252
|
-
});
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
set -e
|
|
3
|
-
|
|
4
|
-
BACKUP_DIR="${BACKUP_DIR:-/backups/clickhouse}"
|
|
5
|
-
RETENTION_DAYS="${RETENTION_DAYS:-7}"
|
|
6
|
-
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
|
7
|
-
BACKUP_NAME="gurulu_${TIMESTAMP}"
|
|
8
|
-
|
|
9
|
-
mkdir -p "$BACKUP_DIR"
|
|
10
|
-
|
|
11
|
-
echo "[backup] Starting ClickHouse backup..."
|
|
12
|
-
docker exec gurulu-clickhouse clickhouse-client --query "BACKUP DATABASE gurulu TO Disk('backups', '${BACKUP_NAME}')" 2>/dev/null || {
|
|
13
|
-
echo "[backup] Native backup not available, using clickhouse-client dump..."
|
|
14
|
-
TABLES=$(docker exec gurulu-clickhouse clickhouse-client --query "SHOW TABLES FROM gurulu" 2>/dev/null)
|
|
15
|
-
|
|
16
|
-
mkdir -p "${BACKUP_DIR}/${BACKUP_NAME}"
|
|
17
|
-
for table in $TABLES; do
|
|
18
|
-
echo "[backup] Dumping table: $table"
|
|
19
|
-
docker exec gurulu-clickhouse clickhouse-client --query "SELECT * FROM gurulu.${table} FORMAT Native" > "${BACKUP_DIR}/${BACKUP_NAME}/${table}.native" 2>/dev/null || true
|
|
20
|
-
done
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
echo "[backup] ClickHouse backup complete."
|
|
24
|
-
|
|
25
|
-
# Cleanup
|
|
26
|
-
find "$BACKUP_DIR" -maxdepth 1 -name "gurulu_*" -mtime +$RETENTION_DAYS -exec rm -rf {} +
|
|
27
|
-
echo "[backup] Cleaned backups older than ${RETENTION_DAYS} days."
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
set -e
|
|
3
|
-
|
|
4
|
-
BACKUP_DIR="${BACKUP_DIR:-/backups/postgres}"
|
|
5
|
-
RETENTION_DAYS="${RETENTION_DAYS:-7}"
|
|
6
|
-
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
|
7
|
-
FILENAME="gurulu_${TIMESTAMP}.sql.gz"
|
|
8
|
-
|
|
9
|
-
mkdir -p "$BACKUP_DIR"
|
|
10
|
-
|
|
11
|
-
echo "[backup] Starting PostgreSQL backup..."
|
|
12
|
-
docker exec gurulu-postgres pg_dump -U gurulu gurulu | gzip > "${BACKUP_DIR}/${FILENAME}"
|
|
13
|
-
|
|
14
|
-
echo "[backup] Backup saved: ${BACKUP_DIR}/${FILENAME}"
|
|
15
|
-
echo "[backup] Size: $(du -h "${BACKUP_DIR}/${FILENAME}" | cut -f1)"
|
|
16
|
-
|
|
17
|
-
# Cleanup old backups
|
|
18
|
-
find "$BACKUP_DIR" -name "gurulu_*.sql.gz" -mtime +$RETENTION_DAYS -delete
|
|
19
|
-
echo "[backup] Cleaned backups older than ${RETENTION_DAYS} days."
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
import { execSync } from 'node:child_process';
|
|
2
|
-
import { readdirSync, readFileSync, existsSync } from 'node:fs';
|
|
3
|
-
import { join } from 'node:path';
|
|
4
|
-
|
|
5
|
-
const RETRY_COUNT = parseInt(process.env.MIGRATION_RETRY_COUNT || '5');
|
|
6
|
-
const RETRY_DELAY = parseInt(process.env.MIGRATION_RETRY_DELAY_MS || '3000');
|
|
7
|
-
const STRICT = process.env.BOOTSTRAP_STRICT !== 'false';
|
|
8
|
-
const ENABLED = process.env.SCHEMA_BOOTSTRAP_ENABLED !== 'false';
|
|
9
|
-
|
|
10
|
-
if (!ENABLED) {
|
|
11
|
-
console.log('[bootstrap] Skipped (SCHEMA_BOOTSTRAP_ENABLED=false)');
|
|
12
|
-
process.exit(0);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function sleep(ms) {
|
|
16
|
-
return new Promise((r) => setTimeout(r, ms));
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function exec(cmd) {
|
|
20
|
-
console.log(`[bootstrap] $ ${cmd}`);
|
|
21
|
-
return execSync(cmd, { stdio: 'inherit' });
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
async function retry(fn, label) {
|
|
25
|
-
for (let i = 1; i <= RETRY_COUNT; i++) {
|
|
26
|
-
try {
|
|
27
|
-
await fn();
|
|
28
|
-
return;
|
|
29
|
-
} catch (err) {
|
|
30
|
-
console.error(`[bootstrap] ${label} attempt ${i}/${RETRY_COUNT} failed:`, err.message);
|
|
31
|
-
if (i === RETRY_COUNT) throw err;
|
|
32
|
-
await sleep(RETRY_DELAY);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
async function main() {
|
|
38
|
-
console.log('[bootstrap] Starting runtime schema bootstrap...');
|
|
39
|
-
|
|
40
|
-
// Step 1/2: Prisma migrate deploy — see /scripts/bootstrap-runtime-schema.mjs
|
|
41
|
-
// for the FA-11 #3 / Sprint I-ops Faz 2 rationale.
|
|
42
|
-
if (process.env.POSTGRES_AUTO_MIGRATE !== 'false') {
|
|
43
|
-
console.log('\n[bootstrap] Step 1/2: Prisma migrate deploy');
|
|
44
|
-
await retry(async () => {
|
|
45
|
-
exec('npx prisma migrate deploy');
|
|
46
|
-
}, 'migrate deploy');
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// Step 2/2: SQL hooks
|
|
50
|
-
if (process.env.POSTGRES_AUTO_MIGRATE !== 'false') {
|
|
51
|
-
console.log('\n[bootstrap] Step 2/2: SQL hooks');
|
|
52
|
-
const hooksDir = join(process.cwd(), 'prisma', 'sql-hooks');
|
|
53
|
-
|
|
54
|
-
if (existsSync(hooksDir)) {
|
|
55
|
-
const files = readdirSync(hooksDir)
|
|
56
|
-
.filter((f) => f.endsWith('.sql'))
|
|
57
|
-
.sort();
|
|
58
|
-
|
|
59
|
-
for (const file of files) {
|
|
60
|
-
console.log(`[bootstrap] Executing hook: ${file}`);
|
|
61
|
-
const filePath = join(hooksDir, file);
|
|
62
|
-
exec(`npx prisma db execute --file "${filePath}"`);
|
|
63
|
-
}
|
|
64
|
-
console.log(`[bootstrap] ${files.length} SQL hook(s) executed.`);
|
|
65
|
-
} else {
|
|
66
|
-
console.log('[bootstrap] No sql-hooks directory found, skipping.');
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
// Step 3/3 (db push drift catch) removed — drift is now a hard fail.
|
|
70
|
-
|
|
71
|
-
// ClickHouse migration
|
|
72
|
-
if (process.env.CLICKHOUSE_AUTO_MIGRATE !== 'false') {
|
|
73
|
-
console.log('\n[bootstrap] ClickHouse migration');
|
|
74
|
-
const nodeIds = (process.env.CLICKHOUSE_MIGRATION_NODE_IDS || 'tr-01').split(',');
|
|
75
|
-
console.log(`[bootstrap] Node IDs: ${nodeIds.join(', ')}`);
|
|
76
|
-
// ClickHouse migrations are handled by init script in Docker
|
|
77
|
-
console.log('[bootstrap] ClickHouse init handled by Docker entrypoint.');
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
console.log('\n[bootstrap] ✓ Schema bootstrap complete.');
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
main().catch((err) => {
|
|
84
|
-
console.error('[bootstrap] Fatal error:', err);
|
|
85
|
-
if (STRICT) process.exit(1);
|
|
86
|
-
else console.warn('[bootstrap] BOOTSTRAP_STRICT=false, continuing despite error.');
|
|
87
|
-
});
|
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
import Stripe from 'stripe';
|
|
2
|
-
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
3
|
-
|
|
4
|
-
const STRIPE_KEY = process.env.STRIPE_SECRET_KEY;
|
|
5
|
-
if (!STRIPE_KEY) {
|
|
6
|
-
console.error('[stripe] STRIPE_SECRET_KEY not set. Skipping bootstrap.');
|
|
7
|
-
process.exit(1);
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
if (STRIPE_KEY.startsWith('sk_live_')) {
|
|
11
|
-
console.warn(
|
|
12
|
-
'[stripe] WARNING: Using a LIVE Stripe key. Products and prices will be created in production.'
|
|
13
|
-
);
|
|
14
|
-
const rl = await import('node:readline');
|
|
15
|
-
const iface = rl.createInterface({ input: process.stdin, output: process.stdout });
|
|
16
|
-
const answer = await new Promise((resolve) =>
|
|
17
|
-
iface.question('[stripe] Continue? (yes/no): ', resolve)
|
|
18
|
-
);
|
|
19
|
-
iface.close();
|
|
20
|
-
if (answer.trim().toLowerCase() !== 'yes') {
|
|
21
|
-
console.log('[stripe] Aborted.');
|
|
22
|
-
process.exit(0);
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const stripe = new Stripe(STRIPE_KEY, {
|
|
27
|
-
apiVersion: '2026-03-25.dahlia',
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
const PRODUCTS = [
|
|
31
|
-
{
|
|
32
|
-
name: 'Gurulu Starter',
|
|
33
|
-
key: 'STARTER',
|
|
34
|
-
monthlyPrice: 2900, // cents
|
|
35
|
-
annualPrice: 27840, // 29 * 12 * 0.8 = $278.40 → $278
|
|
36
|
-
events: 5_000_000,
|
|
37
|
-
},
|
|
38
|
-
{
|
|
39
|
-
name: 'Gurulu Growth',
|
|
40
|
-
key: 'GROWTH',
|
|
41
|
-
monthlyPrice: 9900,
|
|
42
|
-
annualPrice: 95040, // 99 * 12 * 0.8 = $950.40 → $950
|
|
43
|
-
events: 25_000_000,
|
|
44
|
-
},
|
|
45
|
-
{
|
|
46
|
-
name: 'Gurulu Business',
|
|
47
|
-
key: 'BUSINESS',
|
|
48
|
-
monthlyPrice: 29900,
|
|
49
|
-
annualPrice: 287040, // 299 * 12 * 0.8 = $2870.40 → $2870
|
|
50
|
-
events: 100_000_000,
|
|
51
|
-
},
|
|
52
|
-
];
|
|
53
|
-
|
|
54
|
-
async function main() {
|
|
55
|
-
console.log('[stripe] Starting Stripe bootstrap...\n');
|
|
56
|
-
|
|
57
|
-
const envUpdates = {};
|
|
58
|
-
|
|
59
|
-
for (const product of PRODUCTS) {
|
|
60
|
-
console.log(`[stripe] Creating product: ${product.name}`);
|
|
61
|
-
|
|
62
|
-
// Check if product already exists by listing and filtering on metadata
|
|
63
|
-
const allProducts = await stripe.products.list({ limit: 100, active: true });
|
|
64
|
-
const matchingProduct = allProducts.data.find(
|
|
65
|
-
(p) => p.metadata?.gurulu_key === product.key
|
|
66
|
-
);
|
|
67
|
-
|
|
68
|
-
let stripeProduct;
|
|
69
|
-
if (matchingProduct) {
|
|
70
|
-
stripeProduct = matchingProduct;
|
|
71
|
-
console.log(` → Found existing product: ${stripeProduct.id}`);
|
|
72
|
-
} else {
|
|
73
|
-
stripeProduct = await stripe.products.create({
|
|
74
|
-
name: product.name,
|
|
75
|
-
metadata: {
|
|
76
|
-
gurulu_key: product.key,
|
|
77
|
-
events: String(product.events),
|
|
78
|
-
},
|
|
79
|
-
});
|
|
80
|
-
console.log(` → Created product: ${stripeProduct.id}`);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// Fetch all existing prices for this product
|
|
84
|
-
const existingPrices = await stripe.prices.list({
|
|
85
|
-
product: stripeProduct.id,
|
|
86
|
-
type: 'recurring',
|
|
87
|
-
active: true,
|
|
88
|
-
limit: 100,
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
// Monthly price
|
|
92
|
-
let monthlyPrice = existingPrices.data.find(
|
|
93
|
-
(p) => p.recurring?.interval === 'month' && p.unit_amount === product.monthlyPrice
|
|
94
|
-
);
|
|
95
|
-
|
|
96
|
-
if (!monthlyPrice) {
|
|
97
|
-
monthlyPrice = await stripe.prices.create({
|
|
98
|
-
product: stripeProduct.id,
|
|
99
|
-
unit_amount: product.monthlyPrice,
|
|
100
|
-
currency: 'usd',
|
|
101
|
-
recurring: { interval: 'month' },
|
|
102
|
-
metadata: { gurulu_key: product.key, interval: 'monthly' },
|
|
103
|
-
});
|
|
104
|
-
console.log(` → Created monthly price: ${monthlyPrice.id} ($${product.monthlyPrice / 100}/mo)`);
|
|
105
|
-
} else {
|
|
106
|
-
console.log(` → Found monthly price: ${monthlyPrice.id}`);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// Annual price
|
|
110
|
-
let annualPrice = existingPrices.data.find(
|
|
111
|
-
(p) => p.recurring?.interval === 'year' && p.unit_amount === product.annualPrice
|
|
112
|
-
);
|
|
113
|
-
|
|
114
|
-
if (!annualPrice) {
|
|
115
|
-
annualPrice = await stripe.prices.create({
|
|
116
|
-
product: stripeProduct.id,
|
|
117
|
-
unit_amount: product.annualPrice,
|
|
118
|
-
currency: 'usd',
|
|
119
|
-
recurring: { interval: 'year' },
|
|
120
|
-
metadata: { gurulu_key: product.key, interval: 'annual' },
|
|
121
|
-
});
|
|
122
|
-
console.log(` → Created annual price: ${annualPrice.id} ($${product.annualPrice / 100}/yr)`);
|
|
123
|
-
} else {
|
|
124
|
-
console.log(` → Found annual price: ${annualPrice.id}`);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
envUpdates[`STRIPE_${product.key}_MONTHLY_PRICE_ID`] = monthlyPrice.id;
|
|
128
|
-
envUpdates[`STRIPE_${product.key}_ANNUAL_PRICE_ID`] = annualPrice.id;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// Update .env file
|
|
132
|
-
const envPath = '.env';
|
|
133
|
-
let envContent = '';
|
|
134
|
-
if (existsSync(envPath)) {
|
|
135
|
-
envContent = readFileSync(envPath, 'utf-8');
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
for (const [key, value] of Object.entries(envUpdates)) {
|
|
139
|
-
const regex = new RegExp(`^${key}=.*$`, 'm');
|
|
140
|
-
if (regex.test(envContent)) {
|
|
141
|
-
envContent = envContent.replace(regex, `${key}=${value}`);
|
|
142
|
-
} else {
|
|
143
|
-
envContent += `\n${key}=${value}`;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
writeFileSync(envPath, envContent.trimEnd() + '\n');
|
|
148
|
-
|
|
149
|
-
console.log('\n[stripe] ✓ Bootstrap complete. Price IDs written to .env:');
|
|
150
|
-
for (const [key, value] of Object.entries(envUpdates)) {
|
|
151
|
-
console.log(` ${key}=${value}`);
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
main().catch((err) => {
|
|
156
|
-
console.error('[stripe] Bootstrap failed:', err.message);
|
|
157
|
-
process.exit(1);
|
|
158
|
-
});
|