@gurulu/cli 0.4.7 → 1.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.
- 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 +25751 -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 +33 -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 +25326 -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/dist/lib/detect.d.ts +27 -0
- package/dist/lib/detect.d.ts.map +1 -0
- package/dist/lib/detect.js +106 -0
- package/dist/lib/exec-install.d.ts +21 -0
- package/dist/lib/exec-install.d.ts.map +1 -0
- package/dist/lib/install-plan.d.ts +25 -0
- package/dist/lib/install-plan.d.ts.map +1 -0
- package/dist/lib/install-plan.js +161 -0
- package/package.json +51 -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,300 +0,0 @@
|
|
|
1
|
-
// scripts/patches/index.cjs — Phase 16 A1 patch engine registry.
|
|
2
|
-
//
|
|
3
|
-
// Each patcher exports { name, detect, plan, apply, rollback }. The CLI
|
|
4
|
-
// resolves them in priority order via `resolvePatcher()`. Backups and patch
|
|
5
|
-
// logs are produced by shared helpers in this file.
|
|
6
|
-
//
|
|
7
|
-
// Pure Node, no deps. Regex-based patching (ts-morph not available).
|
|
8
|
-
|
|
9
|
-
const fs = require('fs');
|
|
10
|
-
const path = require('path');
|
|
11
|
-
const crypto = require('crypto');
|
|
12
|
-
|
|
13
|
-
// ---------------------------------------------------------------------------
|
|
14
|
-
// BEGIN: Agent A (Phase 18.7) — Framework patcher registration
|
|
15
|
-
// Script-tag injection patchers. Order matters for auto-detection: more
|
|
16
|
-
// specific frameworks first (Next.js before generic Vite+React, Remix
|
|
17
|
-
// before Vue, NestJS/Fastify server-only last so SPA frontends in the
|
|
18
|
-
// same repo win).
|
|
19
|
-
// ---------------------------------------------------------------------------
|
|
20
|
-
const nextAppRouter = require('./nextjs-app-router.patch.cjs');
|
|
21
|
-
const nextPages = require('./nextjs-pages.patch.cjs');
|
|
22
|
-
const remix = require('./remix.patch.cjs');
|
|
23
|
-
const sveltekit = require('./sveltekit.patch.cjs');
|
|
24
|
-
const astro = require('./astro.patch.cjs');
|
|
25
|
-
const viteReact = require('./vite-react.patch.cjs');
|
|
26
|
-
const vue = require('./vue.patch.cjs');
|
|
27
|
-
const nestjs = require('./nestjs.patch.cjs');
|
|
28
|
-
const fastify = require('./fastify.patch.cjs');
|
|
29
|
-
const express = require('./express.patch.cjs');
|
|
30
|
-
|
|
31
|
-
const PATCHERS = [
|
|
32
|
-
nextAppRouter,
|
|
33
|
-
nextPages,
|
|
34
|
-
remix,
|
|
35
|
-
sveltekit,
|
|
36
|
-
astro,
|
|
37
|
-
viteReact,
|
|
38
|
-
vue,
|
|
39
|
-
nestjs,
|
|
40
|
-
fastify,
|
|
41
|
-
express,
|
|
42
|
-
];
|
|
43
|
-
|
|
44
|
-
const PATCHER_BY_NAME = {
|
|
45
|
-
// Sprint D / D3 — `nextjs` (bare) is the alias most agents emit. Resolve it
|
|
46
|
-
// to the App Router patcher (the modern default). The Pages Router can
|
|
47
|
-
// still be selected explicitly via `nextjs-pages`. Auto-detection (in the
|
|
48
|
-
// PATCHERS array above) handles the case where a project actually uses
|
|
49
|
-
// Pages Router and the agent passed `auto`.
|
|
50
|
-
nextjs: nextAppRouter,
|
|
51
|
-
'nextjs-app': nextAppRouter,
|
|
52
|
-
'nextjs-app-router': nextAppRouter,
|
|
53
|
-
'nextjs-pages': nextPages,
|
|
54
|
-
remix,
|
|
55
|
-
sveltekit,
|
|
56
|
-
astro,
|
|
57
|
-
'vite-react': viteReact,
|
|
58
|
-
vue,
|
|
59
|
-
vue3: vue,
|
|
60
|
-
nestjs,
|
|
61
|
-
fastify,
|
|
62
|
-
express,
|
|
63
|
-
};
|
|
64
|
-
// ---------------------------------------------------------------------------
|
|
65
|
-
// END: Agent A (Phase 18.7) — Framework patcher registration
|
|
66
|
-
// NOTE for Agent B: Auto-instrument dispatcher goes in its own section below
|
|
67
|
-
// (or in scripts/patches/auto-instrument/index.cjs). Do not inline it in the
|
|
68
|
-
// PATCHERS array above — those are script-tag patchers only.
|
|
69
|
-
// ---------------------------------------------------------------------------
|
|
70
|
-
|
|
71
|
-
// ---------------------------------------------------------------------------
|
|
72
|
-
// BEGIN: Agent B (Phase 18.7) — Auto-instrumentation dispatcher
|
|
73
|
-
// Route-handler instrumentation modules live in `./auto-instrument/*.cjs`.
|
|
74
|
-
// They share a singleton-helper utility that keeps `src/lib/gurulu.ts`,
|
|
75
|
-
// `package.json`, and `.env.local` / `.env` in sync. The dispatcher below
|
|
76
|
-
// is a thin wrapper that `scripts/gurulu-agentic-install.mjs` calls when
|
|
77
|
-
// `--auto-instrument` is set. Changes produced by auto-instrument modules
|
|
78
|
-
// are stamped `type: 'auto-instrument'` so `applyAutoInstrumentPlan()` can
|
|
79
|
-
// append them to the existing `.gurulu/patch-log.json` (rather than
|
|
80
|
-
// overwriting the script-tag entries) — `rollback()` then restores both
|
|
81
|
-
// patch generations in a single pass.
|
|
82
|
-
// ---------------------------------------------------------------------------
|
|
83
|
-
const autoInstrumentDispatcher = require('./auto-instrument/index.cjs');
|
|
84
|
-
|
|
85
|
-
function autoInstrumentDispatch(framework, ctx, events) {
|
|
86
|
-
return autoInstrumentDispatcher.dispatch(framework, ctx, events);
|
|
87
|
-
}
|
|
88
|
-
// ---------------------------------------------------------------------------
|
|
89
|
-
// END: Agent B (Phase 18.7) — Auto-instrumentation dispatcher
|
|
90
|
-
// ---------------------------------------------------------------------------
|
|
91
|
-
|
|
92
|
-
// Tracker tag context — a small object the patchers stamp into the code they inject.
|
|
93
|
-
function buildInjection(opts) {
|
|
94
|
-
const siteId = opts.siteId || '';
|
|
95
|
-
const tenantId = opts.tenantId || '';
|
|
96
|
-
const publishableKey = opts.publishableKey || '';
|
|
97
|
-
const src = opts.scriptSrc || 'https://gurulu.io/t.js';
|
|
98
|
-
return { siteId, tenantId, publishableKey, scriptSrc: src };
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function resolvePatcher(repoRoot, frameworkHint) {
|
|
102
|
-
if (frameworkHint && frameworkHint !== 'auto') {
|
|
103
|
-
const p = PATCHER_BY_NAME[frameworkHint];
|
|
104
|
-
if (!p) return { patcher: null, detection: null, error: `Unknown framework: ${frameworkHint}` };
|
|
105
|
-
const detection = p.detect(repoRoot);
|
|
106
|
-
return { patcher: p, detection: detection || { framework: p.name, files: [] } };
|
|
107
|
-
}
|
|
108
|
-
for (const p of PATCHERS) {
|
|
109
|
-
const detection = p.detect(repoRoot);
|
|
110
|
-
if (detection) return { patcher: p, detection };
|
|
111
|
-
}
|
|
112
|
-
return { patcher: null, detection: null };
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// Backup one file into .gurulu-backup/<timestamp>/<relPath>.
|
|
116
|
-
function backupFile(repoRoot, relPath, timestamp) {
|
|
117
|
-
const abs = path.join(repoRoot, relPath);
|
|
118
|
-
const original = fs.readFileSync(abs, 'utf8');
|
|
119
|
-
const backupDir = path.join(repoRoot, '.gurulu-backup', timestamp);
|
|
120
|
-
const backupPath = path.join(backupDir, relPath);
|
|
121
|
-
fs.mkdirSync(path.dirname(backupPath), { recursive: true });
|
|
122
|
-
fs.writeFileSync(backupPath, original, 'utf8');
|
|
123
|
-
return { backupPath: path.relative(repoRoot, backupPath), original };
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function hashContent(text) {
|
|
127
|
-
return crypto.createHash('sha256').update(text).digest('hex').slice(0, 16);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function ensureGitignore(repoRoot) {
|
|
131
|
-
const gi = path.join(repoRoot, '.gitignore');
|
|
132
|
-
let contents = '';
|
|
133
|
-
if (fs.existsSync(gi)) contents = fs.readFileSync(gi, 'utf8');
|
|
134
|
-
if (!/\.gurulu-backup/.test(contents)) {
|
|
135
|
-
const append = (contents.endsWith('\n') || !contents ? '' : '\n') + '.gurulu-backup/\n';
|
|
136
|
-
fs.writeFileSync(gi, contents + append, 'utf8');
|
|
137
|
-
return true;
|
|
138
|
-
}
|
|
139
|
-
return false;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// Utility: produce a plain unified-diff-ish output for dry-run (tiny, no deps).
|
|
143
|
-
function unifiedDiff(relPath, before, after) {
|
|
144
|
-
if (before === after) return `--- a/${relPath}\n+++ b/${relPath}\n(no changes)\n`;
|
|
145
|
-
const beforeLines = before.split('\n');
|
|
146
|
-
const afterLines = after.split('\n');
|
|
147
|
-
const lines = [`--- a/${relPath}`, `+++ b/${relPath}`];
|
|
148
|
-
// Naive: show all "-" then all "+" — sufficient for alpha dry-run display.
|
|
149
|
-
for (const l of beforeLines) lines.push(`-${l}`);
|
|
150
|
-
for (const l of afterLines) lines.push(`+${l}`);
|
|
151
|
-
return lines.join('\n') + '\n';
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// Plan → { changes: [{ relPath, before, after, reason }], notes: [] }
|
|
155
|
-
// Apply consumes a plan, writes files, creates backups, writes patch-log.
|
|
156
|
-
async function applyPlan(repoRoot, patcher, plan) {
|
|
157
|
-
if (!plan || !plan.changes || plan.changes.length === 0) {
|
|
158
|
-
return { applied: false, reason: 'no-changes', files: [] };
|
|
159
|
-
}
|
|
160
|
-
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
161
|
-
const fileRecords = [];
|
|
162
|
-
for (const change of plan.changes) {
|
|
163
|
-
const abs = path.join(repoRoot, change.relPath);
|
|
164
|
-
// Ensure parent dir exists (for new files added by patcher if any).
|
|
165
|
-
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
166
|
-
let backupRel = null;
|
|
167
|
-
if (fs.existsSync(abs)) {
|
|
168
|
-
const { backupPath } = backupFile(repoRoot, change.relPath, timestamp);
|
|
169
|
-
backupRel = backupPath;
|
|
170
|
-
}
|
|
171
|
-
fs.writeFileSync(abs, change.after, 'utf8');
|
|
172
|
-
fileRecords.push({
|
|
173
|
-
path: change.relPath,
|
|
174
|
-
hash: hashContent(change.after),
|
|
175
|
-
backupPath: backupRel,
|
|
176
|
-
type: change.type || 'script-tag',
|
|
177
|
-
});
|
|
178
|
-
}
|
|
179
|
-
ensureGitignore(repoRoot);
|
|
180
|
-
const logDir = path.join(repoRoot, '.gurulu');
|
|
181
|
-
fs.mkdirSync(logDir, { recursive: true });
|
|
182
|
-
const logFile = path.join(logDir, 'patch-log.json');
|
|
183
|
-
const log = {
|
|
184
|
-
appliedAt: new Date().toISOString(),
|
|
185
|
-
framework: patcher.name,
|
|
186
|
-
timestamp,
|
|
187
|
-
files: fileRecords,
|
|
188
|
-
rollbackAvailable: true,
|
|
189
|
-
};
|
|
190
|
-
fs.writeFileSync(logFile, JSON.stringify(log, null, 2) + '\n', 'utf8');
|
|
191
|
-
return { applied: true, log, files: fileRecords };
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// ---------------------------------------------------------------------------
|
|
195
|
-
// Phase 18.7 B6 — applyAutoInstrumentPlan
|
|
196
|
-
// Applies the auto-instrument plan (helper + route handler changes) using
|
|
197
|
-
// the same backup mechanism as applyPlan, but APPENDS to an existing
|
|
198
|
-
// `.gurulu/patch-log.json` rather than overwriting it. This lets the
|
|
199
|
-
// script-tag patch and the auto-instrument patch coexist under a single
|
|
200
|
-
// rollback pass.
|
|
201
|
-
// ---------------------------------------------------------------------------
|
|
202
|
-
async function applyAutoInstrumentPlan(repoRoot, plan) {
|
|
203
|
-
if (!plan || !plan.changes || plan.changes.length === 0) {
|
|
204
|
-
return { applied: false, reason: 'no-changes', files: [] };
|
|
205
|
-
}
|
|
206
|
-
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
207
|
-
const fileRecords = [];
|
|
208
|
-
for (const change of plan.changes) {
|
|
209
|
-
const abs = path.join(repoRoot, change.relPath);
|
|
210
|
-
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
211
|
-
let backupRel = null;
|
|
212
|
-
if (fs.existsSync(abs)) {
|
|
213
|
-
const { backupPath } = backupFile(repoRoot, change.relPath, timestamp);
|
|
214
|
-
backupRel = backupPath;
|
|
215
|
-
}
|
|
216
|
-
fs.writeFileSync(abs, change.after, 'utf8');
|
|
217
|
-
fileRecords.push({
|
|
218
|
-
path: change.relPath,
|
|
219
|
-
hash: hashContent(change.after),
|
|
220
|
-
backupPath: backupRel,
|
|
221
|
-
type: change.type || 'auto-instrument',
|
|
222
|
-
});
|
|
223
|
-
}
|
|
224
|
-
ensureGitignore(repoRoot);
|
|
225
|
-
const logDir = path.join(repoRoot, '.gurulu');
|
|
226
|
-
fs.mkdirSync(logDir, { recursive: true });
|
|
227
|
-
const logFile = path.join(logDir, 'patch-log.json');
|
|
228
|
-
let log;
|
|
229
|
-
if (fs.existsSync(logFile)) {
|
|
230
|
-
try {
|
|
231
|
-
log = JSON.parse(fs.readFileSync(logFile, 'utf8'));
|
|
232
|
-
} catch {
|
|
233
|
-
log = null;
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
if (!log) {
|
|
237
|
-
log = {
|
|
238
|
-
appliedAt: new Date().toISOString(),
|
|
239
|
-
framework: plan.framework || 'auto-instrument',
|
|
240
|
-
timestamp,
|
|
241
|
-
files: [],
|
|
242
|
-
rollbackAvailable: true,
|
|
243
|
-
};
|
|
244
|
-
}
|
|
245
|
-
log.files = [...(log.files || []), ...fileRecords];
|
|
246
|
-
log.autoInstrumentAppliedAt = new Date().toISOString();
|
|
247
|
-
log.autoInstrumentTimestamp = timestamp;
|
|
248
|
-
log.rollbackAvailable = true;
|
|
249
|
-
fs.writeFileSync(logFile, JSON.stringify(log, null, 2) + '\n', 'utf8');
|
|
250
|
-
return { applied: true, log, files: fileRecords };
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
function rollback(repoRoot) {
|
|
254
|
-
const logFile = path.join(repoRoot, '.gurulu', 'patch-log.json');
|
|
255
|
-
if (!fs.existsSync(logFile)) {
|
|
256
|
-
return { rolledBack: false, reason: 'no-patch-log' };
|
|
257
|
-
}
|
|
258
|
-
const log = JSON.parse(fs.readFileSync(logFile, 'utf8'));
|
|
259
|
-
if (!log.rollbackAvailable) return { rolledBack: false, reason: 'rollback-unavailable' };
|
|
260
|
-
const restored = [];
|
|
261
|
-
for (const f of log.files) {
|
|
262
|
-
if (!f.backupPath) {
|
|
263
|
-
// File was newly created by patcher — delete it.
|
|
264
|
-
const abs = path.join(repoRoot, f.path);
|
|
265
|
-
if (fs.existsSync(abs)) fs.unlinkSync(abs);
|
|
266
|
-
restored.push({ path: f.path, action: 'deleted' });
|
|
267
|
-
continue;
|
|
268
|
-
}
|
|
269
|
-
const backupAbs = path.join(repoRoot, f.backupPath);
|
|
270
|
-
if (!fs.existsSync(backupAbs)) {
|
|
271
|
-
restored.push({ path: f.path, action: 'missing-backup' });
|
|
272
|
-
continue;
|
|
273
|
-
}
|
|
274
|
-
const original = fs.readFileSync(backupAbs, 'utf8');
|
|
275
|
-
fs.writeFileSync(path.join(repoRoot, f.path), original, 'utf8');
|
|
276
|
-
restored.push({ path: f.path, action: 'restored' });
|
|
277
|
-
}
|
|
278
|
-
// Mark log as consumed.
|
|
279
|
-
log.rollbackAvailable = false;
|
|
280
|
-
log.rolledBackAt = new Date().toISOString();
|
|
281
|
-
fs.writeFileSync(logFile, JSON.stringify(log, null, 2) + '\n', 'utf8');
|
|
282
|
-
return { rolledBack: true, files: restored };
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
module.exports = {
|
|
286
|
-
PATCHERS,
|
|
287
|
-
PATCHER_BY_NAME,
|
|
288
|
-
resolvePatcher,
|
|
289
|
-
buildInjection,
|
|
290
|
-
backupFile,
|
|
291
|
-
hashContent,
|
|
292
|
-
ensureGitignore,
|
|
293
|
-
unifiedDiff,
|
|
294
|
-
applyPlan,
|
|
295
|
-
rollback,
|
|
296
|
-
// Phase 18.7 B — auto-instrumentation exports.
|
|
297
|
-
autoInstrumentDispatch,
|
|
298
|
-
autoInstrument: autoInstrumentDispatcher,
|
|
299
|
-
applyAutoInstrumentPlan,
|
|
300
|
-
};
|
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
// nestjs.patch.cjs — Phase 18.7 A1
|
|
2
|
-
//
|
|
3
|
-
// NestJS is a server-side framework. Most real-world NestJS apps serve a
|
|
4
|
-
// separate frontend (React/Vue/Angular) or bundle a static client under
|
|
5
|
-
// `client/dist/` / `public/`. When we find a static `index.html` we patch
|
|
6
|
-
// it with a plain <script> tag. Otherwise we emit a detection note telling
|
|
7
|
-
// the operator to use --auto-instrument for server-side tracking (Phase
|
|
8
|
-
// 18.7 Agent B territory).
|
|
9
|
-
|
|
10
|
-
const fs = require('fs');
|
|
11
|
-
const path = require('path');
|
|
12
|
-
|
|
13
|
-
const NAME = 'nestjs';
|
|
14
|
-
const HTML_CANDIDATES = [
|
|
15
|
-
'public/index.html',
|
|
16
|
-
'client/dist/index.html',
|
|
17
|
-
'client/build/index.html',
|
|
18
|
-
'dist/client/index.html',
|
|
19
|
-
];
|
|
20
|
-
const SERVER_CANDIDATES = [
|
|
21
|
-
'src/main.ts',
|
|
22
|
-
'src/main.js',
|
|
23
|
-
];
|
|
24
|
-
|
|
25
|
-
const MARKER = 'data-gurulu-install="1"';
|
|
26
|
-
|
|
27
|
-
function looksLikeNest(repoRoot) {
|
|
28
|
-
// Detect either via package.json `@nestjs/core` dep or the idiomatic
|
|
29
|
-
// `NestFactory.create(...)` bootstrap call in main.ts.
|
|
30
|
-
const pkgPath = path.join(repoRoot, 'package.json');
|
|
31
|
-
if (fs.existsSync(pkgPath)) {
|
|
32
|
-
try {
|
|
33
|
-
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
34
|
-
const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
|
|
35
|
-
if (deps['@nestjs/core']) return true;
|
|
36
|
-
} catch {
|
|
37
|
-
/* ignore malformed package.json */
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
for (const rel of SERVER_CANDIDATES) {
|
|
41
|
-
const abs = path.join(repoRoot, rel);
|
|
42
|
-
if (fs.existsSync(abs)) {
|
|
43
|
-
const source = fs.readFileSync(abs, 'utf8');
|
|
44
|
-
if (/NestFactory\.create\s*\(/.test(source) || /@nestjs\/core/.test(source)) {
|
|
45
|
-
return true;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
return false;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function detect(repoRoot) {
|
|
53
|
-
if (!looksLikeNest(repoRoot)) return null;
|
|
54
|
-
for (const rel of HTML_CANDIDATES) {
|
|
55
|
-
if (fs.existsSync(path.join(repoRoot, rel))) {
|
|
56
|
-
return { framework: NAME, files: [rel] };
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
// Server-only — no frontend to patch. Return detection with empty file list
|
|
60
|
-
// so the caller can surface the note.
|
|
61
|
-
return { framework: NAME, files: [], serverOnly: true };
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function buildScriptTag(injection) {
|
|
65
|
-
const pkAttr = injection.publishableKey
|
|
66
|
-
? ` data-gurulu-publishable-key="${injection.publishableKey}"`
|
|
67
|
-
: '';
|
|
68
|
-
return (
|
|
69
|
-
` <script\n` +
|
|
70
|
-
` src="${injection.scriptSrc}"\n` +
|
|
71
|
-
` data-gurulu-site-id="${injection.siteId}"\n` +
|
|
72
|
-
` data-gurulu-tenant-id="${injection.tenantId}"${pkAttr}\n` +
|
|
73
|
-
` data-features="errors,replay,advanced"\n` +
|
|
74
|
-
` ${MARKER}\n` +
|
|
75
|
-
` async\n` +
|
|
76
|
-
` ></script>\n`
|
|
77
|
-
);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function injectScriptTag(source, injection) {
|
|
81
|
-
if (source.includes(MARKER)) return source;
|
|
82
|
-
const tag = buildScriptTag(injection);
|
|
83
|
-
if (/<\/body>/.test(source)) {
|
|
84
|
-
return source.replace(/<\/body>/, `${tag} </body>`);
|
|
85
|
-
}
|
|
86
|
-
return source + '\n' + tag;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function plan(ctx) {
|
|
90
|
-
const detection = detect(ctx.repoRoot);
|
|
91
|
-
if (!detection) return { changes: [], notes: ['no-nestjs'] };
|
|
92
|
-
if (detection.serverOnly) {
|
|
93
|
-
return {
|
|
94
|
-
changes: [],
|
|
95
|
-
notes: [
|
|
96
|
-
'server-only; script injection skipped, use --auto-instrument for server-side tracking',
|
|
97
|
-
],
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
const changes = [];
|
|
101
|
-
for (const rel of detection.files) {
|
|
102
|
-
const abs = path.join(ctx.repoRoot, rel);
|
|
103
|
-
const before = fs.readFileSync(abs, 'utf8');
|
|
104
|
-
const after = injectScriptTag(before, ctx.injection);
|
|
105
|
-
if (after !== before) {
|
|
106
|
-
changes.push({ relPath: rel, before, after, reason: 'inject-script-tag' });
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
return { changes, notes: [] };
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
module.exports = { name: NAME, detect, plan };
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
// nextjs-app-router.patch.cjs — Phase 16 A1
|
|
2
|
-
//
|
|
3
|
-
// Detects a Next.js App Router layout (`src/app/layout.tsx` or `app/layout.tsx`)
|
|
4
|
-
// and injects a <Script> tag loaded via next/script that points to the Gurulu
|
|
5
|
-
// Web SDK tracker (`https://gurulu.io/t.js` by default).
|
|
6
|
-
//
|
|
7
|
-
// Regex-based: fast alpha, good enough for most idiomatic RootLayouts.
|
|
8
|
-
|
|
9
|
-
const fs = require('fs');
|
|
10
|
-
const path = require('path');
|
|
11
|
-
|
|
12
|
-
const NAME = 'nextjs-app-router';
|
|
13
|
-
const CANDIDATES = [
|
|
14
|
-
'src/app/layout.tsx',
|
|
15
|
-
'src/app/layout.jsx',
|
|
16
|
-
'app/layout.tsx',
|
|
17
|
-
'app/layout.jsx',
|
|
18
|
-
];
|
|
19
|
-
|
|
20
|
-
const MARKER = 'data-gurulu-install="1"';
|
|
21
|
-
|
|
22
|
-
function detect(repoRoot) {
|
|
23
|
-
for (const rel of CANDIDATES) {
|
|
24
|
-
if (fs.existsSync(path.join(repoRoot, rel))) {
|
|
25
|
-
return { framework: NAME, files: [rel] };
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
return null;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function buildScriptTag(injection) {
|
|
32
|
-
const pkAttr = injection.publishableKey
|
|
33
|
-
? ` data-gurulu-publishable-key=${JSON.stringify(injection.publishableKey)}\n`
|
|
34
|
-
: '';
|
|
35
|
-
return (
|
|
36
|
-
` <Script\n` +
|
|
37
|
-
` id="gurulu-tracker"\n` +
|
|
38
|
-
` src=${JSON.stringify(injection.scriptSrc)}\n` +
|
|
39
|
-
` strategy="afterInteractive"\n` +
|
|
40
|
-
` data-gurulu-site-id=${JSON.stringify(injection.siteId)}\n` +
|
|
41
|
-
` data-gurulu-tenant-id=${JSON.stringify(injection.tenantId)}\n` +
|
|
42
|
-
pkAttr +
|
|
43
|
-
` data-features="errors,replay,advanced"\n` +
|
|
44
|
-
` ${MARKER}\n` +
|
|
45
|
-
` />\n`
|
|
46
|
-
);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function ensureScriptImport(source) {
|
|
50
|
-
if (/from\s+['"]next\/script['"]/.test(source)) return source;
|
|
51
|
-
// Insert after the last import statement (or at top).
|
|
52
|
-
const importRegex = /^(?:import[\s\S]*?;\s*\n)+/m;
|
|
53
|
-
const m = source.match(importRegex);
|
|
54
|
-
const inject = `import Script from 'next/script';\n`;
|
|
55
|
-
if (m) {
|
|
56
|
-
const end = m.index + m[0].length;
|
|
57
|
-
return source.slice(0, end) + inject + source.slice(end);
|
|
58
|
-
}
|
|
59
|
-
return inject + source;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function injectScriptTag(source, injection) {
|
|
63
|
-
if (source.includes(MARKER)) return source; // idempotent
|
|
64
|
-
// Also detect existing tracker tags (manual or from other install methods)
|
|
65
|
-
if (source.includes('data-site-id=') || source.includes('data-gurulu-site-id=') || source.includes('/t.js')) return source;
|
|
66
|
-
const tag = buildScriptTag(injection);
|
|
67
|
-
// Prefer to insert just before </body>. Fall back to before the closing
|
|
68
|
-
// top-level fragment/element.
|
|
69
|
-
if (/<\/body>/.test(source)) {
|
|
70
|
-
return source.replace(/<\/body>/, `${tag} </body>`);
|
|
71
|
-
}
|
|
72
|
-
// Insert before the final closing tag of children area — best-effort.
|
|
73
|
-
return source.replace(/(\n\s*<\/(?:html|main|div)>\s*\)\s*;?\s*\}\s*$)/, (m) => `\n${tag}${m}`);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function plan(ctx) {
|
|
77
|
-
const detection = detect(ctx.repoRoot);
|
|
78
|
-
if (!detection) return { changes: [], notes: ['no-layout-found'] };
|
|
79
|
-
const changes = [];
|
|
80
|
-
for (const rel of detection.files) {
|
|
81
|
-
const abs = path.join(ctx.repoRoot, rel);
|
|
82
|
-
const before = fs.readFileSync(abs, 'utf8');
|
|
83
|
-
let after = ensureScriptImport(before);
|
|
84
|
-
after = injectScriptTag(after, ctx.injection);
|
|
85
|
-
if (after !== before) {
|
|
86
|
-
changes.push({ relPath: rel, before, after, reason: 'inject-next-script' });
|
|
87
|
-
} else {
|
|
88
|
-
changes.push({ relPath: rel, before, after, reason: 'already-installed', skip: true });
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
return {
|
|
92
|
-
changes: changes.filter((c) => !c.skip),
|
|
93
|
-
notes: changes.filter((c) => c.skip).map((c) => `skip:${c.relPath}`),
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
module.exports = { name: NAME, detect, plan };
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
// nextjs-pages.patch.cjs — Phase 16 A1
|
|
2
|
-
//
|
|
3
|
-
// Detects a legacy Next.js Pages Router `_app` file and injects a useEffect
|
|
4
|
-
// that appends the Gurulu tracker <script> to document.head.
|
|
5
|
-
|
|
6
|
-
const fs = require('fs');
|
|
7
|
-
const path = require('path');
|
|
8
|
-
|
|
9
|
-
const NAME = 'nextjs-pages';
|
|
10
|
-
const CANDIDATES = [
|
|
11
|
-
'pages/_app.tsx',
|
|
12
|
-
'pages/_app.jsx',
|
|
13
|
-
'src/pages/_app.tsx',
|
|
14
|
-
'src/pages/_app.jsx',
|
|
15
|
-
];
|
|
16
|
-
|
|
17
|
-
const MARKER = '/* gurulu:install */';
|
|
18
|
-
|
|
19
|
-
function detect(repoRoot) {
|
|
20
|
-
for (const rel of CANDIDATES) {
|
|
21
|
-
if (fs.existsSync(path.join(repoRoot, rel))) {
|
|
22
|
-
return { framework: NAME, files: [rel] };
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
return null;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function ensureUseEffectImport(source) {
|
|
29
|
-
if (/from\s+['"]react['"]/.test(source)) {
|
|
30
|
-
if (/useEffect/.test(source)) return source;
|
|
31
|
-
return source.replace(/import\s+([^;]*?)from\s+(['"])react\2/, (m, names, q) => {
|
|
32
|
-
if (/\{[^}]*\}/.test(names)) {
|
|
33
|
-
return m.replace(/\{([^}]*)\}/, (mm, inner) => `{${inner.trim()}, useEffect }`);
|
|
34
|
-
}
|
|
35
|
-
return `${m.trim()}\nimport { useEffect } from 'react'`;
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
return `import { useEffect } from 'react';\n` + source;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function buildHook(injection) {
|
|
42
|
-
const pkLine = injection.publishableKey
|
|
43
|
-
? ` s.setAttribute('data-gurulu-publishable-key', ${JSON.stringify(injection.publishableKey)});\n`
|
|
44
|
-
: '';
|
|
45
|
-
return (
|
|
46
|
-
` ${MARKER}\n` +
|
|
47
|
-
` useEffect(() => {\n` +
|
|
48
|
-
` if (typeof window === 'undefined') return;\n` +
|
|
49
|
-
` if (document.querySelector('script[data-gurulu-install]')) return;\n` +
|
|
50
|
-
` const s = document.createElement('script');\n` +
|
|
51
|
-
` s.src = ${JSON.stringify(injection.scriptSrc)};\n` +
|
|
52
|
-
` s.async = true;\n` +
|
|
53
|
-
` s.setAttribute('data-gurulu-install', '1');\n` +
|
|
54
|
-
` s.setAttribute('data-gurulu-site-id', ${JSON.stringify(injection.siteId)});\n` +
|
|
55
|
-
` s.setAttribute('data-gurulu-tenant-id', ${JSON.stringify(injection.tenantId)});\n` +
|
|
56
|
-
pkLine +
|
|
57
|
-
` s.setAttribute('data-features', 'errors,replay,advanced');\n` +
|
|
58
|
-
` document.head.appendChild(s);\n` +
|
|
59
|
-
` }, []);\n`
|
|
60
|
-
);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function injectHook(source, injection) {
|
|
64
|
-
if (source.includes(MARKER)) return source;
|
|
65
|
-
const hook = buildHook(injection);
|
|
66
|
-
// Match: function MyApp({ Component, pageProps }) { ... — insert hook after `{`.
|
|
67
|
-
const fnMatch = source.match(/function\s+\w+\s*\([^)]*\)\s*\{/);
|
|
68
|
-
if (fnMatch) {
|
|
69
|
-
const end = fnMatch.index + fnMatch[0].length;
|
|
70
|
-
return source.slice(0, end) + '\n' + hook + source.slice(end);
|
|
71
|
-
}
|
|
72
|
-
// Arrow form: `const MyApp = ({ Component, pageProps }) => {`
|
|
73
|
-
const arrowMatch = source.match(/const\s+\w+\s*=\s*\([^)]*\)\s*=>\s*\{/);
|
|
74
|
-
if (arrowMatch) {
|
|
75
|
-
const end = arrowMatch.index + arrowMatch[0].length;
|
|
76
|
-
return source.slice(0, end) + '\n' + hook + source.slice(end);
|
|
77
|
-
}
|
|
78
|
-
return source;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function plan(ctx) {
|
|
82
|
-
const detection = detect(ctx.repoRoot);
|
|
83
|
-
if (!detection) return { changes: [], notes: ['no-_app-found'] };
|
|
84
|
-
const changes = [];
|
|
85
|
-
for (const rel of detection.files) {
|
|
86
|
-
const abs = path.join(ctx.repoRoot, rel);
|
|
87
|
-
const before = fs.readFileSync(abs, 'utf8');
|
|
88
|
-
let after = injectHook(before, ctx.injection);
|
|
89
|
-
if (after !== before) after = ensureUseEffectImport(after);
|
|
90
|
-
if (after !== before) {
|
|
91
|
-
changes.push({ relPath: rel, before, after, reason: 'inject-useeffect' });
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
return { changes, notes: [] };
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
module.exports = { name: NAME, detect, plan };
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
// remix.patch.cjs — Phase 18.7 A1
|
|
2
|
-
//
|
|
3
|
-
// Detects a Remix app by the presence of `app/root.tsx` (or `.jsx`) and
|
|
4
|
-
// injects a plain <script> tag before the `<Scripts />` component inside
|
|
5
|
-
// the root layout's <body>. Remix's <Scripts /> handles hydration; we keep
|
|
6
|
-
// our tracker as a vanilla async <script> so it's independent of the React
|
|
7
|
-
// runtime.
|
|
8
|
-
|
|
9
|
-
const fs = require('fs');
|
|
10
|
-
const path = require('path');
|
|
11
|
-
|
|
12
|
-
const NAME = 'remix';
|
|
13
|
-
const CANDIDATES = [
|
|
14
|
-
'app/root.tsx',
|
|
15
|
-
'app/root.jsx',
|
|
16
|
-
];
|
|
17
|
-
|
|
18
|
-
const MARKER = 'data-gurulu-install="1"';
|
|
19
|
-
|
|
20
|
-
function detect(repoRoot) {
|
|
21
|
-
for (const rel of CANDIDATES) {
|
|
22
|
-
if (fs.existsSync(path.join(repoRoot, rel))) {
|
|
23
|
-
return { framework: NAME, files: [rel] };
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
return null;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function buildScriptTag(injection) {
|
|
30
|
-
const pkAttr = injection.publishableKey
|
|
31
|
-
? ` data-gurulu-publishable-key=${JSON.stringify(injection.publishableKey)}\n`
|
|
32
|
-
: '';
|
|
33
|
-
return (
|
|
34
|
-
` <script\n` +
|
|
35
|
-
` src=${JSON.stringify(injection.scriptSrc)}\n` +
|
|
36
|
-
` data-gurulu-site-id=${JSON.stringify(injection.siteId)}\n` +
|
|
37
|
-
` data-gurulu-tenant-id=${JSON.stringify(injection.tenantId)}\n` +
|
|
38
|
-
pkAttr +
|
|
39
|
-
` data-features="errors,replay,advanced"\n` +
|
|
40
|
-
` ${MARKER}\n` +
|
|
41
|
-
` async\n` +
|
|
42
|
-
` />\n`
|
|
43
|
-
);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function injectScriptTag(source, injection) {
|
|
47
|
-
if (source.includes(MARKER)) return source;
|
|
48
|
-
const tag = buildScriptTag(injection);
|
|
49
|
-
// Prefer to insert just before <Scripts /> (self-closing or open tag).
|
|
50
|
-
if (/<Scripts\s*\/?>/.test(source)) {
|
|
51
|
-
return source.replace(/(<Scripts\s*\/?>)/, `${tag} $1`);
|
|
52
|
-
}
|
|
53
|
-
// Fallback: before </body>.
|
|
54
|
-
if (/<\/body>/.test(source)) {
|
|
55
|
-
return source.replace(/<\/body>/, `${tag} </body>`);
|
|
56
|
-
}
|
|
57
|
-
return source;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function plan(ctx) {
|
|
61
|
-
const detection = detect(ctx.repoRoot);
|
|
62
|
-
if (!detection) return { changes: [], notes: ['no-remix-root'] };
|
|
63
|
-
const changes = [];
|
|
64
|
-
for (const rel of detection.files) {
|
|
65
|
-
const abs = path.join(ctx.repoRoot, rel);
|
|
66
|
-
const before = fs.readFileSync(abs, 'utf8');
|
|
67
|
-
const after = injectScriptTag(before, ctx.injection);
|
|
68
|
-
if (after !== before) {
|
|
69
|
-
changes.push({ relPath: rel, before, after, reason: 'inject-script-tag' });
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
return { changes, notes: [] };
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
module.exports = { name: NAME, detect, plan };
|