foglift-sensor 0.1.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/README.md +32 -0
- package/dist/chunk-TUBJ5Q45.js +336 -0
- package/dist/cli.d.ts +4 -0
- package/dist/cli.js +66 -0
- package/dist/installer.d.ts +48 -0
- package/dist/installer.js +14 -0
- package/package.json +48 -0
package/README.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Foglift Sensor CLI
|
|
2
|
+
|
|
3
|
+
Install the Foglift Sensor in a code-built site with the publishable workspace
|
|
4
|
+
identifier from Foglift AI Traffic (it is a capability identifier, not a secret
|
|
5
|
+
API key):
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx foglift-sensor@latest init --token PUBLIC_WORKSPACE_ID
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
The CLI detects Next.js or Cloudflare Workers, installs the stable
|
|
12
|
+
`@foglift/tracker` runtime, and composes around the existing request boundary or
|
|
13
|
+
Worker code. For Next.js 16 it preserves and exports `proxy`; for older Next.js
|
|
14
|
+
projects it keeps the `middleware` convention. Root/`src` placement and static
|
|
15
|
+
custom `pageExtensions` are respected. The original source is preserved
|
|
16
|
+
byte-for-byte in a neighboring `*.foglift-original.*` file and is never replaced
|
|
17
|
+
on a later run.
|
|
18
|
+
|
|
19
|
+
The tracker sends the request hostname with the workspace capability. Foglift
|
|
20
|
+
accepts an event only when both identify the same workspace; a hostname alone
|
|
21
|
+
cannot write analytics. If the URL cannot be inferred from package metadata, pass
|
|
22
|
+
`--url https://example.com`.
|
|
23
|
+
|
|
24
|
+
Equivalent one-shot commands:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pnpm dlx foglift-sensor@latest init --token PUBLIC_WORKSPACE_ID
|
|
28
|
+
yarn dlx foglift-sensor@latest init --token PUBLIC_WORKSPACE_ID
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
After deploying the patched site, verify a recognized crawler request in
|
|
32
|
+
[Foglift AI Traffic](https://foglift.io/app/data/crawlers).
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
// src/installer.ts
|
|
2
|
+
import {
|
|
3
|
+
existsSync,
|
|
4
|
+
readFileSync,
|
|
5
|
+
renameSync,
|
|
6
|
+
writeFileSync
|
|
7
|
+
} from "fs";
|
|
8
|
+
import { randomBytes } from "crypto";
|
|
9
|
+
import { basename, dirname, extname, join, relative, resolve } from "path";
|
|
10
|
+
var SENSOR_MARKER = "Foglift Sensor installer \u2014 generated file";
|
|
11
|
+
var TRACKER_PACKAGE = "@foglift/tracker@latest";
|
|
12
|
+
function readJson(path) {
|
|
13
|
+
try {
|
|
14
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
15
|
+
} catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function packageHas(root, dependency) {
|
|
20
|
+
const manifest = readJson(join(root, "package.json"));
|
|
21
|
+
if (!manifest) return false;
|
|
22
|
+
return ["dependencies", "devDependencies", "peerDependencies"].some((field) => {
|
|
23
|
+
const values = manifest[field];
|
|
24
|
+
return Boolean(values && typeof values === "object" && dependency in values);
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
function parseWranglerEntry(root) {
|
|
28
|
+
for (const name of ["wrangler.jsonc", "wrangler.json", "wrangler.toml"]) {
|
|
29
|
+
const path = join(root, name);
|
|
30
|
+
if (!existsSync(path)) continue;
|
|
31
|
+
const source = readFileSync(path, "utf8");
|
|
32
|
+
if (name.endsWith(".toml")) {
|
|
33
|
+
const match = source.match(/^\s*main\s*=\s*["']([^"']+)["']/m);
|
|
34
|
+
return { source: name, entry: match?.[1] ?? "src/index.ts" };
|
|
35
|
+
}
|
|
36
|
+
const withoutComments = source.replace(/\/\*[\s\S]*?\*\//g, "").replace(/^\s*\/\/.*$/gm, "").replace(/,\s*([}\]])/g, "$1");
|
|
37
|
+
try {
|
|
38
|
+
const parsed = JSON.parse(withoutComments);
|
|
39
|
+
return {
|
|
40
|
+
source: name,
|
|
41
|
+
entry: typeof parsed.main === "string" ? parsed.main : "src/index.ts"
|
|
42
|
+
};
|
|
43
|
+
} catch {
|
|
44
|
+
return { source: name, entry: "src/index.ts" };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
function detectCodeStack(cwd) {
|
|
50
|
+
const root = resolve(cwd);
|
|
51
|
+
const manifest = readJson(join(root, "package.json"));
|
|
52
|
+
const dependencies = {
|
|
53
|
+
...manifest?.dependencies ?? {},
|
|
54
|
+
...manifest?.devDependencies ?? {}
|
|
55
|
+
};
|
|
56
|
+
if ("next" in dependencies) return { kind: "nextjs", source: "package.json" };
|
|
57
|
+
const wrangler = parseWranglerEntry(root);
|
|
58
|
+
if (wrangler) return { kind: "cloudflare", ...wrangler };
|
|
59
|
+
const packageSignals = [
|
|
60
|
+
["remix", "@remix-run/node"],
|
|
61
|
+
["nuxt", "nuxt"],
|
|
62
|
+
["sveltekit", "@sveltejs/kit"],
|
|
63
|
+
["astro", "astro"],
|
|
64
|
+
["express", "express"]
|
|
65
|
+
];
|
|
66
|
+
for (const [kind, dependency] of packageSignals) {
|
|
67
|
+
if (dependency in dependencies) return { kind, source: "package.json" };
|
|
68
|
+
}
|
|
69
|
+
return { kind: "unknown", source: null };
|
|
70
|
+
}
|
|
71
|
+
function runtimeInstallCommand(cwd) {
|
|
72
|
+
if (existsSync(join(cwd, "pnpm-lock.yaml"))) {
|
|
73
|
+
return { command: "pnpm", args: ["add", TRACKER_PACKAGE] };
|
|
74
|
+
}
|
|
75
|
+
if (existsSync(join(cwd, "yarn.lock"))) {
|
|
76
|
+
return { command: "yarn", args: ["add", TRACKER_PACKAGE] };
|
|
77
|
+
}
|
|
78
|
+
return { command: "npm", args: ["install", TRACKER_PACKAGE] };
|
|
79
|
+
}
|
|
80
|
+
function normalizeProjectUrl(value) {
|
|
81
|
+
if (typeof value !== "string" || !value.trim()) return null;
|
|
82
|
+
try {
|
|
83
|
+
const url = new URL(value.includes("://") ? value : `https://${value}`);
|
|
84
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return null;
|
|
85
|
+
return `${url.protocol}//${url.hostname.toLowerCase()}`;
|
|
86
|
+
} catch {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function inferProjectUrl(cwd) {
|
|
91
|
+
const fromEnv = normalizeProjectUrl(
|
|
92
|
+
process.env.FOGLIFT_SENSOR_URL ?? process.env.VERCEL_PROJECT_PRODUCTION_URL
|
|
93
|
+
);
|
|
94
|
+
if (fromEnv) return fromEnv;
|
|
95
|
+
const manifest = readJson(join(cwd, "package.json"));
|
|
96
|
+
return normalizeProjectUrl(manifest?.homepage);
|
|
97
|
+
}
|
|
98
|
+
async function verifySensorEvent(input) {
|
|
99
|
+
const targetOrigin = normalizeProjectUrl(input.url);
|
|
100
|
+
if (!targetOrigin) throw new Error("A valid deployed http(s) URL is required for verification");
|
|
101
|
+
const nonce = input.nonce ?? randomBytes(16).toString("hex");
|
|
102
|
+
if (!/^[a-f0-9]{32}$/.test(nonce)) throw new Error("Invalid verification nonce");
|
|
103
|
+
const fetcher = input.fetcher ?? fetch;
|
|
104
|
+
const probeUrl = `${targetOrigin}/.well-known/foglift-sensor/${nonce}`;
|
|
105
|
+
const probe = await fetcher(probeUrl, {
|
|
106
|
+
headers: { "User-Agent": "FogliftSensorVerify/1.0 GPTBot" },
|
|
107
|
+
redirect: "manual",
|
|
108
|
+
signal: AbortSignal.timeout(1e4)
|
|
109
|
+
});
|
|
110
|
+
const fogliftOrigin = (input.fogliftOrigin ?? "https://foglift.io").replace(/\/$/, "");
|
|
111
|
+
const statusUrl = new URL("/api/v1/sensor-install/verify", fogliftOrigin);
|
|
112
|
+
statusUrl.searchParams.set("domain", new URL(targetOrigin).hostname);
|
|
113
|
+
statusUrl.searchParams.set("nonce", nonce);
|
|
114
|
+
const attempts = Math.max(1, Math.min(input.attempts ?? 15, 30));
|
|
115
|
+
const wait = input.wait ?? ((milliseconds) => new Promise((resolvePromise) => setTimeout(resolvePromise, milliseconds)));
|
|
116
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
117
|
+
if (attempt > 0) await wait(1e3);
|
|
118
|
+
const status = await fetcher(statusUrl.toString(), {
|
|
119
|
+
headers: { Accept: "application/json" },
|
|
120
|
+
signal: AbortSignal.timeout(1e4)
|
|
121
|
+
});
|
|
122
|
+
if (!status.ok) throw new Error(`Foglift verification returned ${status.status}`);
|
|
123
|
+
const body = await status.json();
|
|
124
|
+
if (body.observed === true) {
|
|
125
|
+
return { observed: true, nonce, probeStatus: probe.status };
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return { observed: false, nonce, probeStatus: probe.status };
|
|
129
|
+
}
|
|
130
|
+
function sidecarPath(entry) {
|
|
131
|
+
const extension = extname(entry);
|
|
132
|
+
return join(dirname(entry), `${basename(entry, extension)}.foglift-original${extension}`);
|
|
133
|
+
}
|
|
134
|
+
function importPath(from, target) {
|
|
135
|
+
const extension = extname(target);
|
|
136
|
+
const withoutExtension = target.slice(0, -extension.length);
|
|
137
|
+
const path = relative(dirname(from), withoutExtension).replaceAll("\\", "/");
|
|
138
|
+
return path.startsWith(".") ? path : `./${path}`;
|
|
139
|
+
}
|
|
140
|
+
function trackerOptions(token, next) {
|
|
141
|
+
const fields = [token ? `siteToken: ${JSON.stringify(token)}` : null, next ? `next: ${next}` : null].filter(Boolean);
|
|
142
|
+
return `{ ${fields.join(", ")} }`;
|
|
143
|
+
}
|
|
144
|
+
function nextWrapper(entry, original, token, convention) {
|
|
145
|
+
if (!original) {
|
|
146
|
+
return `// ${SENSOR_MARKER}
|
|
147
|
+
import { trackAITraffic } from "@foglift/tracker/nextjs";
|
|
148
|
+
|
|
149
|
+
export const ${convention} = trackAITraffic(${trackerOptions(token)});
|
|
150
|
+
|
|
151
|
+
export const config = {
|
|
152
|
+
matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
|
|
153
|
+
};
|
|
154
|
+
`;
|
|
155
|
+
}
|
|
156
|
+
const specifier = importPath(entry, original);
|
|
157
|
+
const candidate = `preservedModule.${convention} ?? preservedModule.default`;
|
|
158
|
+
return `// ${SENSOR_MARKER}
|
|
159
|
+
import { trackAITraffic, type NextMiddleware } from "@foglift/tracker/nextjs";
|
|
160
|
+
import * as existingModule from ${JSON.stringify(specifier)};
|
|
161
|
+
|
|
162
|
+
const preservedModule = existingModule as Record<string, unknown>;
|
|
163
|
+
const existingMiddlewareCandidate = ${candidate};
|
|
164
|
+
|
|
165
|
+
if (typeof existingMiddlewareCandidate !== "function") {
|
|
166
|
+
throw new Error("Foglift Sensor could not find the preserved ${convention} export");
|
|
167
|
+
}
|
|
168
|
+
const existingMiddleware = existingMiddlewareCandidate as NextMiddleware;
|
|
169
|
+
|
|
170
|
+
export * from ${JSON.stringify(specifier)};
|
|
171
|
+
export const ${convention} = trackAITraffic(${trackerOptions(token, "existingMiddleware")});
|
|
172
|
+
`;
|
|
173
|
+
}
|
|
174
|
+
function cloudflareWrapper(entry, original, token) {
|
|
175
|
+
const specifier = importPath(entry, original);
|
|
176
|
+
return `// ${SENSOR_MARKER}
|
|
177
|
+
import { trackAITrafficWorker } from "@foglift/tracker/cloudflare";
|
|
178
|
+
import existingWorker from ${JSON.stringify(specifier)};
|
|
179
|
+
|
|
180
|
+
if (!existingWorker || typeof existingWorker.fetch !== "function") {
|
|
181
|
+
throw new Error("Foglift Sensor requires a default Worker object with a fetch handler");
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const existingFetch = existingWorker.fetch.bind(existingWorker);
|
|
185
|
+
const sensorFetch = trackAITrafficWorker(${trackerOptions(token, "existingFetch")});
|
|
186
|
+
|
|
187
|
+
export default {
|
|
188
|
+
...existingWorker,
|
|
189
|
+
fetch: sensorFetch,
|
|
190
|
+
};
|
|
191
|
+
`;
|
|
192
|
+
}
|
|
193
|
+
function containsMarker(path) {
|
|
194
|
+
return existsSync(path) && readFileSync(path, "utf8").includes(SENSOR_MARKER);
|
|
195
|
+
}
|
|
196
|
+
function preserveAndWrite(entry, wrapper) {
|
|
197
|
+
const original = sidecarPath(entry);
|
|
198
|
+
if (existsSync(original)) {
|
|
199
|
+
throw new Error(`Refusing to replace existing preserved file: ${original}`);
|
|
200
|
+
}
|
|
201
|
+
const temporary = `${entry}.foglift-tmp`;
|
|
202
|
+
writeFileSync(temporary, wrapper, { flag: "wx" });
|
|
203
|
+
try {
|
|
204
|
+
renameSync(entry, original);
|
|
205
|
+
renameSync(temporary, entry);
|
|
206
|
+
} catch (error) {
|
|
207
|
+
if (existsSync(original) && !existsSync(entry)) renameSync(original, entry);
|
|
208
|
+
throw error;
|
|
209
|
+
}
|
|
210
|
+
return [entry, original];
|
|
211
|
+
}
|
|
212
|
+
function fallbackInstructions() {
|
|
213
|
+
return [
|
|
214
|
+
"Foglift did not recognize a supported code adapter.",
|
|
215
|
+
"For nginx, mirror recognized crawler requests to https://foglift.io/api/v1/crawler-analytics and forward the original Host header.",
|
|
216
|
+
"For generic middleware, report recognized crawler requests from the server and preserve the original request hostname.",
|
|
217
|
+
"Open https://foglift.io/docs/sensor for the safe, compose-without-replacing examples."
|
|
218
|
+
].join("\n");
|
|
219
|
+
}
|
|
220
|
+
function nextMajorVersion(root) {
|
|
221
|
+
const manifest = readJson(join(root, "package.json"));
|
|
222
|
+
const dependencies = {
|
|
223
|
+
...manifest?.dependencies ?? {},
|
|
224
|
+
...manifest?.devDependencies ?? {}
|
|
225
|
+
};
|
|
226
|
+
const version = dependencies.next;
|
|
227
|
+
if (typeof version !== "string") return null;
|
|
228
|
+
const match = version.match(/(?:^|[^0-9])(\d+)(?:\.|$)/);
|
|
229
|
+
return match ? Number.parseInt(match[1], 10) : null;
|
|
230
|
+
}
|
|
231
|
+
function configuredPageExtensions(root) {
|
|
232
|
+
for (const name of [
|
|
233
|
+
"next.config.ts",
|
|
234
|
+
"next.config.mts",
|
|
235
|
+
"next.config.cts",
|
|
236
|
+
"next.config.js",
|
|
237
|
+
"next.config.mjs",
|
|
238
|
+
"next.config.cjs"
|
|
239
|
+
]) {
|
|
240
|
+
const path = join(root, name);
|
|
241
|
+
if (!existsSync(path)) continue;
|
|
242
|
+
const source = readFileSync(path, "utf8");
|
|
243
|
+
const array = source.match(/\bpageExtensions\s*:\s*\[([\s\S]*?)\]/)?.[1];
|
|
244
|
+
if (!array) return [];
|
|
245
|
+
return Array.from(array.matchAll(/["'`]([^"'`]+)["'`]/g), (match) => match[1]).filter(
|
|
246
|
+
(extension) => !extension.includes("/") && /(?:^|\.)(?:[cm]?[jt]sx?)$/.test(extension)
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
return [];
|
|
250
|
+
}
|
|
251
|
+
function conventionFileNames(convention, pageExtensions) {
|
|
252
|
+
const configured = pageExtensions.map((extension) => `${convention}.${extension}`);
|
|
253
|
+
return [...configured, `${convention}.ts`, `${convention}.js`, `${convention}.mjs`];
|
|
254
|
+
}
|
|
255
|
+
function nextEntry(root) {
|
|
256
|
+
const pageExtensions = configuredPageExtensions(root);
|
|
257
|
+
const preferredConvention = (nextMajorVersion(root) ?? 0) >= 16 ? "proxy" : "middleware";
|
|
258
|
+
const conventionOrder = preferredConvention === "proxy" ? ["proxy", "middleware"] : ["middleware", "proxy"];
|
|
259
|
+
for (const convention of conventionOrder) {
|
|
260
|
+
for (const name of conventionFileNames(convention, pageExtensions)) {
|
|
261
|
+
for (const prefix of ["", "src/"]) {
|
|
262
|
+
const path = join(root, `${prefix}${name}`);
|
|
263
|
+
if (existsSync(path)) return { entry: path, convention };
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
const sourceRoot = existsSync(join(root, "src/app")) || existsSync(join(root, "src/pages"));
|
|
268
|
+
const fileName = conventionFileNames(preferredConvention, pageExtensions)[0];
|
|
269
|
+
return {
|
|
270
|
+
entry: join(root, sourceRoot ? "src" : "", fileName),
|
|
271
|
+
convention: preferredConvention
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
function applySensorInstall(input) {
|
|
275
|
+
const root = resolve(input.cwd);
|
|
276
|
+
const stack = detectCodeStack(root);
|
|
277
|
+
const url = normalizeProjectUrl(input.url) ?? inferProjectUrl(root);
|
|
278
|
+
if (!["nextjs", "cloudflare"].includes(stack.kind)) {
|
|
279
|
+
return {
|
|
280
|
+
kind: stack.kind,
|
|
281
|
+
changedFiles: [],
|
|
282
|
+
fallback: `${stack.kind === "unknown" ? "" : `Detected ${stack.kind}.
|
|
283
|
+
`}${fallbackInstructions()}`,
|
|
284
|
+
url
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
if (!input.token?.trim()) {
|
|
288
|
+
throw new Error("A public workspace token is required. Pass --token PUBLIC_WORKSPACE_ID.");
|
|
289
|
+
}
|
|
290
|
+
const supportedStack = stack;
|
|
291
|
+
const nextBoundary = supportedStack.kind === "nextjs" ? nextEntry(root) : null;
|
|
292
|
+
const entry = supportedStack.kind === "nextjs" ? nextBoundary.entry : join(root, supportedStack.entry);
|
|
293
|
+
if (containsMarker(entry)) {
|
|
294
|
+
return { kind: supportedStack.kind, changedFiles: [], url };
|
|
295
|
+
}
|
|
296
|
+
if (!packageHas(root, "@foglift/tracker")) {
|
|
297
|
+
const install = runtimeInstallCommand(root);
|
|
298
|
+
input.execute(install.command, install.args, root);
|
|
299
|
+
}
|
|
300
|
+
if (supportedStack.kind === "nextjs") {
|
|
301
|
+
if (!existsSync(entry)) {
|
|
302
|
+
writeFileSync(
|
|
303
|
+
entry,
|
|
304
|
+
nextWrapper(entry, null, input.token, nextBoundary.convention),
|
|
305
|
+
{ flag: "wx" }
|
|
306
|
+
);
|
|
307
|
+
return { kind: "nextjs", changedFiles: [entry], url };
|
|
308
|
+
}
|
|
309
|
+
const original2 = sidecarPath(entry);
|
|
310
|
+
return {
|
|
311
|
+
kind: "nextjs",
|
|
312
|
+
changedFiles: preserveAndWrite(
|
|
313
|
+
entry,
|
|
314
|
+
nextWrapper(entry, original2, input.token, nextBoundary.convention)
|
|
315
|
+
),
|
|
316
|
+
url
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
if (!existsSync(entry)) {
|
|
320
|
+
throw new Error(`Cloudflare Worker entry not found: ${relative(root, entry)}`);
|
|
321
|
+
}
|
|
322
|
+
const original = sidecarPath(entry);
|
|
323
|
+
return {
|
|
324
|
+
kind: "cloudflare",
|
|
325
|
+
changedFiles: preserveAndWrite(entry, cloudflareWrapper(entry, original, input.token)),
|
|
326
|
+
url
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export {
|
|
331
|
+
detectCodeStack,
|
|
332
|
+
runtimeInstallCommand,
|
|
333
|
+
inferProjectUrl,
|
|
334
|
+
verifySensorEvent,
|
|
335
|
+
applySensorInstall
|
|
336
|
+
};
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
applySensorInstall,
|
|
4
|
+
verifySensorEvent
|
|
5
|
+
} from "./chunk-TUBJ5Q45.js";
|
|
6
|
+
|
|
7
|
+
// src/cli.ts
|
|
8
|
+
import { spawnSync } from "child_process";
|
|
9
|
+
function valueAfter(args, flag) {
|
|
10
|
+
const index = args.indexOf(flag);
|
|
11
|
+
return index >= 0 ? args[index + 1] : void 0;
|
|
12
|
+
}
|
|
13
|
+
var execute = (command, args, cwd) => {
|
|
14
|
+
const result = spawnSync(command, args, { cwd, stdio: "inherit" });
|
|
15
|
+
if (result.error) throw result.error;
|
|
16
|
+
if (result.status !== 0) {
|
|
17
|
+
throw new Error(`${command} ${args.join(" ")} failed with exit ${result.status ?? "unknown"}`);
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
async function run(argv = process.argv.slice(2)) {
|
|
21
|
+
if (argv[0] !== "init") {
|
|
22
|
+
console.error("Usage: npx foglift-sensor@latest init --token PUBLIC_WORKSPACE_ID [--url https://example.com]");
|
|
23
|
+
return 1;
|
|
24
|
+
}
|
|
25
|
+
const result = applySensorInstall({
|
|
26
|
+
cwd: process.cwd(),
|
|
27
|
+
execute,
|
|
28
|
+
url: valueAfter(argv, "--url"),
|
|
29
|
+
token: valueAfter(argv, "--token")
|
|
30
|
+
});
|
|
31
|
+
if (result.fallback) {
|
|
32
|
+
console.log(result.fallback);
|
|
33
|
+
return 0;
|
|
34
|
+
}
|
|
35
|
+
if (result.changedFiles.length === 0) {
|
|
36
|
+
console.log(`Foglift Sensor is already composed with this ${result.kind} project.`);
|
|
37
|
+
} else {
|
|
38
|
+
console.log(`Foglift Sensor composed with ${result.kind}.`);
|
|
39
|
+
for (const file of result.changedFiles) console.log(`- ${file}`);
|
|
40
|
+
}
|
|
41
|
+
if (argv.includes("--verify")) {
|
|
42
|
+
if (!result.url) {
|
|
43
|
+
console.error("Verification needs --url https://your-deployed-site.example");
|
|
44
|
+
return 1;
|
|
45
|
+
}
|
|
46
|
+
const verification = await verifySensorEvent({ url: result.url });
|
|
47
|
+
if (!verification.observed) {
|
|
48
|
+
console.error("Foglift did not observe the new Sensor event. Deploy the patched site, then retry with --verify.");
|
|
49
|
+
return 2;
|
|
50
|
+
}
|
|
51
|
+
console.log(`Foglift Sensor verified (probe HTTP ${verification.probeStatus}; event persisted).`);
|
|
52
|
+
} else {
|
|
53
|
+
console.log("After deployment, rerun this command with --verify to prove a new persisted event.");
|
|
54
|
+
}
|
|
55
|
+
console.log(`AI Traffic: ${result.url ? `https://foglift.io/app/data/crawlers?site=${encodeURIComponent(result.url)}` : "https://foglift.io/app/data/crawlers"}`);
|
|
56
|
+
return 0;
|
|
57
|
+
}
|
|
58
|
+
run().then((code) => {
|
|
59
|
+
process.exitCode = code;
|
|
60
|
+
}).catch((error) => {
|
|
61
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
62
|
+
process.exitCode = 1;
|
|
63
|
+
});
|
|
64
|
+
export {
|
|
65
|
+
run
|
|
66
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
type DetectedCodeStack = {
|
|
2
|
+
kind: "nextjs";
|
|
3
|
+
source: "package.json";
|
|
4
|
+
} | {
|
|
5
|
+
kind: "cloudflare";
|
|
6
|
+
source: string;
|
|
7
|
+
entry: string;
|
|
8
|
+
} | {
|
|
9
|
+
kind: "remix" | "nuxt" | "sveltekit" | "astro" | "express";
|
|
10
|
+
source: "package.json";
|
|
11
|
+
} | {
|
|
12
|
+
kind: "unknown";
|
|
13
|
+
source: null;
|
|
14
|
+
};
|
|
15
|
+
type CommandExecutor = (command: string, args: string[], cwd: string) => void;
|
|
16
|
+
type SensorInstallResult = {
|
|
17
|
+
kind: DetectedCodeStack["kind"];
|
|
18
|
+
changedFiles: string[];
|
|
19
|
+
fallback?: string;
|
|
20
|
+
url: string | null;
|
|
21
|
+
};
|
|
22
|
+
type SensorVerificationResult = {
|
|
23
|
+
observed: boolean;
|
|
24
|
+
nonce: string;
|
|
25
|
+
probeStatus: number;
|
|
26
|
+
};
|
|
27
|
+
declare function detectCodeStack(cwd: string): DetectedCodeStack;
|
|
28
|
+
declare function runtimeInstallCommand(cwd: string): {
|
|
29
|
+
command: string;
|
|
30
|
+
args: string[];
|
|
31
|
+
};
|
|
32
|
+
declare function inferProjectUrl(cwd: string): string | null;
|
|
33
|
+
declare function verifySensorEvent(input: {
|
|
34
|
+
url: string;
|
|
35
|
+
fetcher?: typeof fetch;
|
|
36
|
+
nonce?: string;
|
|
37
|
+
attempts?: number;
|
|
38
|
+
wait?: (milliseconds: number) => Promise<void>;
|
|
39
|
+
fogliftOrigin?: string;
|
|
40
|
+
}): Promise<SensorVerificationResult>;
|
|
41
|
+
declare function applySensorInstall(input: {
|
|
42
|
+
cwd: string;
|
|
43
|
+
execute: CommandExecutor;
|
|
44
|
+
token?: string;
|
|
45
|
+
url?: string;
|
|
46
|
+
}): SensorInstallResult;
|
|
47
|
+
|
|
48
|
+
export { type CommandExecutor, type DetectedCodeStack, type SensorInstallResult, type SensorVerificationResult, applySensorInstall, detectCodeStack, inferProjectUrl, runtimeInstallCommand, verifySensorEvent };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import {
|
|
2
|
+
applySensorInstall,
|
|
3
|
+
detectCodeStack,
|
|
4
|
+
inferProjectUrl,
|
|
5
|
+
runtimeInstallCommand,
|
|
6
|
+
verifySensorEvent
|
|
7
|
+
} from "./chunk-TUBJ5Q45.js";
|
|
8
|
+
export {
|
|
9
|
+
applySensorInstall,
|
|
10
|
+
detectCodeStack,
|
|
11
|
+
inferProjectUrl,
|
|
12
|
+
runtimeInstallCommand,
|
|
13
|
+
verifySensorEvent
|
|
14
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "foglift-sensor",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Install the Foglift Sensor in a code-built site with one command.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"foglift-sensor": "dist/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"main": "./dist/installer.js",
|
|
11
|
+
"types": "./dist/installer.d.ts",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/installer.d.ts",
|
|
15
|
+
"import": "./dist/installer.js"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist",
|
|
20
|
+
"README.md"
|
|
21
|
+
],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "tsup src/cli.ts src/installer.ts --format esm --dts --clean",
|
|
24
|
+
"prepublishOnly": "npm run build"
|
|
25
|
+
},
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=20"
|
|
28
|
+
},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"foglift",
|
|
31
|
+
"sensor",
|
|
32
|
+
"ai-crawler",
|
|
33
|
+
"nextjs",
|
|
34
|
+
"cloudflare",
|
|
35
|
+
"cli"
|
|
36
|
+
],
|
|
37
|
+
"author": "Foglift <watson@foglift.io>",
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"url": "git+https://github.com/banant2/Foglift.git"
|
|
41
|
+
},
|
|
42
|
+
"homepage": "https://foglift.io/sensor",
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@types/node": "^22.0.0",
|
|
45
|
+
"tsup": "^8.0.0",
|
|
46
|
+
"typescript": "^5.6.0"
|
|
47
|
+
}
|
|
48
|
+
}
|