visibly-cli 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/bin/visibly.js ADDED
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env node
2
+ 'use strict'
3
+
4
+ const { runSetup } = require('../src/setup')
5
+
6
+ const args = process.argv.slice(2)
7
+ const command = args[0]
8
+
9
+ if (!command || command === 'help' || command === '--help' || command === '-h') {
10
+ console.log(`
11
+ Visibly CLI
12
+
13
+ Commands:
14
+ setup Instrument your server to track AI bot visits
15
+
16
+ Usage:
17
+ npx visibly setup --domain-id <your-domain-id>
18
+
19
+ Options:
20
+ --domain-id Your Visibly domain ID (found in the dashboard)
21
+ --help Show this help message
22
+ `)
23
+ process.exit(0)
24
+ }
25
+
26
+ if (command === 'setup') {
27
+ // Parse --domain-id flag
28
+ const domainIdIdx = args.indexOf('--domain-id')
29
+ const domainId = domainIdIdx !== -1 ? args[domainIdIdx + 1] : null
30
+
31
+ runSetup(domainId)
32
+ process.exit(0)
33
+ }
34
+
35
+ console.error(`\n Unknown command: ${command}\n Run "npx visibly --help" for usage.\n`)
36
+ process.exit(1)
package/package.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "visibly-cli",
3
+ "version": "0.1.0",
4
+ "description": "Set up Visibly AI bot tracking on your server with one command",
5
+ "bin": {
6
+ "visibly": "bin/visibly.js"
7
+ },
8
+ "main": "./src/setup.js",
9
+ "engines": {
10
+ "node": ">=18"
11
+ },
12
+ "keywords": [
13
+ "visibly",
14
+ "ai",
15
+ "seo",
16
+ "bot-tracking",
17
+ "llm"
18
+ ],
19
+ "license": "MIT"
20
+ }
package/src/detect.js ADDED
@@ -0,0 +1,91 @@
1
+ 'use strict'
2
+
3
+ const fs = require('fs')
4
+ const path = require('path')
5
+
6
+ /**
7
+ * Reads package.json from cwd and returns the detected framework.
8
+ * @returns {'next' | 'express' | 'unknown'}
9
+ */
10
+ function detectFramework(cwd = process.cwd()) {
11
+ const pkgPath = path.join(cwd, 'package.json')
12
+
13
+ if (!fs.existsSync(pkgPath)) {
14
+ return 'unknown'
15
+ }
16
+
17
+ let pkg
18
+ try {
19
+ pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'))
20
+ } catch {
21
+ return 'unknown'
22
+ }
23
+
24
+ const deps = {
25
+ ...pkg.dependencies,
26
+ ...pkg.devDependencies,
27
+ }
28
+
29
+ // Next.js takes precedence — a custom Express server inside a Next.js app
30
+ // is still a Next.js project at its core.
31
+ if (deps['next']) return 'next'
32
+ if (deps['express']) return 'express'
33
+ return 'unknown'
34
+ }
35
+
36
+ /**
37
+ * Finds the middleware file for a Next.js project.
38
+ * Returns the path if it exists, or null if it needs to be created.
39
+ * @returns {{ path: string, exists: boolean }}
40
+ */
41
+ function findNextMiddleware(cwd = process.cwd()) {
42
+ const candidates = [
43
+ 'middleware.ts',
44
+ 'middleware.js',
45
+ 'src/middleware.ts',
46
+ 'src/middleware.js',
47
+ ]
48
+
49
+ for (const rel of candidates) {
50
+ const abs = path.join(cwd, rel)
51
+ if (fs.existsSync(abs)) return { path: abs, exists: true }
52
+ }
53
+
54
+ // Default creation path: src/middleware.ts if src/ exists, else middleware.ts at root
55
+ const srcDir = path.join(cwd, 'src')
56
+ const defaultPath = fs.existsSync(srcDir)
57
+ ? path.join(srcDir, 'middleware.ts')
58
+ : path.join(cwd, 'middleware.ts')
59
+
60
+ return { path: defaultPath, exists: false }
61
+ }
62
+
63
+ /**
64
+ * Finds the Express entry file.
65
+ * @returns {string | null} absolute path, or null if not found
66
+ */
67
+ function findExpressEntry(cwd = process.cwd()) {
68
+ const candidates = [
69
+ 'server.js',
70
+ 'server.ts',
71
+ 'app.js',
72
+ 'app.ts',
73
+ 'index.js',
74
+ 'index.ts',
75
+ 'src/server.js',
76
+ 'src/server.ts',
77
+ 'src/app.js',
78
+ 'src/app.ts',
79
+ 'src/index.js',
80
+ 'src/index.ts',
81
+ ]
82
+
83
+ for (const rel of candidates) {
84
+ const abs = path.join(cwd, rel)
85
+ if (fs.existsSync(abs)) return abs
86
+ }
87
+
88
+ return null
89
+ }
90
+
91
+ module.exports = { detectFramework, findNextMiddleware, findExpressEntry }
@@ -0,0 +1,49 @@
1
+ 'use strict'
2
+
3
+ const fs = require('fs')
4
+ const { expressBlock } = require('./snippets')
5
+
6
+ const ALREADY_INJECTED_MARKER = '// Visibly bot tracking'
7
+
8
+ /**
9
+ * Injects the Visibly tracking middleware into an Express entry file.
10
+ *
11
+ * Strategy: find the line where `express()` is called and assigned
12
+ * (e.g. `const app = express()`) and insert the app.use() block
13
+ * immediately after it. Falls back to appending at end of file.
14
+ */
15
+ function injectIntoExpress(filePath) {
16
+ const source = fs.readFileSync(filePath, 'utf8')
17
+
18
+ // Guard: already injected
19
+ if (source.includes(ALREADY_INJECTED_MARKER)) {
20
+ return { action: 'already_injected', path: filePath }
21
+ }
22
+
23
+ const lines = source.split('\n')
24
+
25
+ // Look for the app initialisation line: `= express()` or `express()`
26
+ let insertIdx = -1
27
+ for (let i = 0; i < lines.length; i++) {
28
+ if (/=\s*express\s*\(/.test(lines[i]) || /^const app\s*=/.test(lines[i])) {
29
+ insertIdx = i
30
+ break
31
+ }
32
+ }
33
+
34
+ const block = expressBlock()
35
+
36
+ if (insertIdx === -1) {
37
+ // Fallback: append at end of file
38
+ const patched = source.trimEnd() + '\n' + block + '\n'
39
+ fs.writeFileSync(filePath, patched, 'utf8')
40
+ return { action: 'appended', path: filePath, warning: 'Could not find Express app initialisation — snippet appended at end of file. Review placement manually.' }
41
+ }
42
+
43
+ // Insert after the found line
44
+ lines.splice(insertIdx + 1, 0, block)
45
+ fs.writeFileSync(filePath, lines.join('\n'), 'utf8')
46
+ return { action: 'injected', path: filePath }
47
+ }
48
+
49
+ module.exports = { injectIntoExpress }
@@ -0,0 +1,71 @@
1
+ 'use strict'
2
+
3
+ const fs = require('fs')
4
+ const path = require('path')
5
+ const { trackingBlock, nextMiddlewareTemplate } = require('./snippets')
6
+
7
+ const ALREADY_INJECTED_MARKER = '// Visibly bot tracking'
8
+
9
+ /**
10
+ * Creates a brand-new middleware.ts at the given path.
11
+ * Used when no middleware file exists yet.
12
+ */
13
+ function createMiddleware(filePath, domainId) {
14
+ fs.mkdirSync(path.dirname(filePath), { recursive: true })
15
+ fs.writeFileSync(filePath, nextMiddlewareTemplate(domainId), 'utf8')
16
+ return { action: 'created', path: filePath }
17
+ }
18
+
19
+ /**
20
+ * Injects the tracking block into an existing middleware file.
21
+ *
22
+ * Strategy: find the last `return NextResponse.next()` or `NextResponse.next()`
23
+ * call and insert the tracking block immediately before it.
24
+ * Uses a line-by-line approach so we don't corrupt the rest of the file.
25
+ */
26
+ function injectIntoMiddleware(filePath) {
27
+ const source = fs.readFileSync(filePath, 'utf8')
28
+
29
+ // Guard: already injected
30
+ if (source.includes(ALREADY_INJECTED_MARKER)) {
31
+ return { action: 'already_injected', path: filePath }
32
+ }
33
+
34
+ const lines = source.split('\n')
35
+
36
+ // Find the last line that contains `return NextResponse.next()`
37
+ // We look from the bottom up so we target the final return in the middleware function.
38
+ let insertIdx = -1
39
+ let indent = ' '
40
+
41
+ for (let i = lines.length - 1; i >= 0; i--) {
42
+ const trimmed = lines[i].trim()
43
+ // Match any return statement that involves NextResponse.next():
44
+ // return NextResponse.next()
45
+ // return response || NextResponse.next()
46
+ // return NextResponse.next({ request })
47
+ if (trimmed.startsWith('return') && trimmed.includes('NextResponse.next(')) {
48
+ insertIdx = i
49
+ // Detect indentation from that line
50
+ const match = lines[i].match(/^(\s+)/)
51
+ if (match) indent = match[1]
52
+ break
53
+ }
54
+ }
55
+
56
+ if (insertIdx === -1) {
57
+ // Fallback: couldn't find a return statement — append to end of file with a warning
58
+ const block = trackingBlock(' ')
59
+ const patched = source.trimEnd() + '\n\n' + block + '\n'
60
+ fs.writeFileSync(filePath, patched, 'utf8')
61
+ return { action: 'appended', path: filePath, warning: 'Could not find "return NextResponse.next()" — snippet appended at end of file. Review placement manually.' }
62
+ }
63
+
64
+ // Insert tracking block before the return line
65
+ const block = trackingBlock(indent)
66
+ lines.splice(insertIdx, 0, block)
67
+ fs.writeFileSync(filePath, lines.join('\n'), 'utf8')
68
+ return { action: 'injected', path: filePath }
69
+ }
70
+
71
+ module.exports = { createMiddleware, injectIntoMiddleware }
package/src/setup.js ADDED
@@ -0,0 +1,122 @@
1
+ 'use strict'
2
+
3
+ const fs = require('fs')
4
+ const path = require('path')
5
+ const { detectFramework, findNextMiddleware, findExpressEntry } = require('./detect')
6
+ const { createMiddleware, injectIntoMiddleware } = require('./inject-next')
7
+ const { injectIntoExpress } = require('./inject-express')
8
+ const { manualSnippet } = require('./snippets')
9
+
10
+ const ENV_KEY = 'VISIBLY_DOMAIN_ID'
11
+
12
+ /**
13
+ * Writes or updates VISIBLY_DOMAIN_ID in .env.local.
14
+ * Creates the file if it doesn't exist.
15
+ * Updates the existing line if the key is already present.
16
+ */
17
+ function writeEnvLocal(domainId, cwd = process.cwd()) {
18
+ const envPath = path.join(cwd, '.env.local')
19
+ const line = `${ENV_KEY}=${domainId}`
20
+
21
+ if (!fs.existsSync(envPath)) {
22
+ fs.writeFileSync(envPath, line + '\n', 'utf8')
23
+ return { action: 'created', path: envPath }
24
+ }
25
+
26
+ let contents = fs.readFileSync(envPath, 'utf8')
27
+
28
+ if (contents.includes(ENV_KEY)) {
29
+ // Replace existing line
30
+ contents = contents.replace(new RegExp(`^${ENV_KEY}=.*$`, 'm'), line)
31
+ fs.writeFileSync(envPath, contents, 'utf8')
32
+ return { action: 'updated', path: envPath }
33
+ }
34
+
35
+ // Append to end (ensure trailing newline before appending)
36
+ if (!contents.endsWith('\n')) contents += '\n'
37
+ fs.writeFileSync(envPath, contents + line + '\n', 'utf8')
38
+ return { action: 'appended', path: envPath }
39
+ }
40
+
41
+ /**
42
+ * Main setup command.
43
+ * @param {string} domainId
44
+ * @param {{ cwd?: string }} options
45
+ */
46
+ function runSetup(domainId, { cwd = process.cwd() } = {}) {
47
+ if (!domainId) {
48
+ console.error('\nError: --domain-id is required.\n')
49
+ console.error(' Usage: npx visibly setup --domain-id <your-domain-id>\n')
50
+ process.exit(1)
51
+ }
52
+
53
+ console.log('\n Setting up Visibly bot tracking...\n')
54
+
55
+ const framework = detectFramework(cwd)
56
+
57
+ // ── Next.js ────────────────────────────────────────────────────────────────
58
+ if (framework === 'next') {
59
+ const { path: mwPath, exists } = findNextMiddleware(cwd)
60
+
61
+ let result
62
+ if (exists) {
63
+ result = injectIntoMiddleware(mwPath)
64
+ } else {
65
+ result = createMiddleware(mwPath, domainId)
66
+ }
67
+
68
+ const relPath = path.relative(cwd, result.path)
69
+
70
+ if (result.action === 'already_injected') {
71
+ console.log(` ✓ Tracking already present in ${relPath} — no changes made.`)
72
+ } else if (result.action === 'created') {
73
+ console.log(` ✓ Created ${relPath}`)
74
+ } else if (result.action === 'injected') {
75
+ console.log(` ✓ Injected tracking into ${relPath}`)
76
+ } else if (result.action === 'appended') {
77
+ console.log(` ✓ Appended tracking to ${relPath}`)
78
+ if (result.warning) console.log(` ⚠ ${result.warning}`)
79
+ }
80
+
81
+ const envResult = writeEnvLocal(domainId, cwd)
82
+ console.log(` ✓ ${envResult.action === 'created' ? 'Created' : 'Updated'} .env.local with ${ENV_KEY}`)
83
+
84
+ console.log('\n Done! Deploy your site to start recording bot visits.\n')
85
+ return
86
+ }
87
+
88
+ // ── Express ────────────────────────────────────────────────────────────────
89
+ if (framework === 'express') {
90
+ const entryPath = findExpressEntry(cwd)
91
+
92
+ if (!entryPath) {
93
+ console.log(' Express detected but entry file not found.')
94
+ console.log(manualSnippet(domainId))
95
+ process.exit(0)
96
+ }
97
+
98
+ const result = injectIntoExpress(entryPath)
99
+ const relPath = path.relative(cwd, result.path)
100
+
101
+ if (result.action === 'already_injected') {
102
+ console.log(` ✓ Tracking already present in ${relPath} — no changes made.`)
103
+ } else if (result.action === 'injected') {
104
+ console.log(` ✓ Injected tracking into ${relPath}`)
105
+ } else if (result.action === 'appended') {
106
+ console.log(` ✓ Appended tracking to ${relPath}`)
107
+ if (result.warning) console.log(` ⚠ ${result.warning}`)
108
+ }
109
+
110
+ const envResult = writeEnvLocal(domainId, cwd)
111
+ console.log(` ✓ ${envResult.action === 'created' ? 'Created' : 'Updated'} .env.local with ${ENV_KEY}`)
112
+
113
+ console.log('\n Done! Deploy your site to start recording bot visits.\n')
114
+ return
115
+ }
116
+
117
+ // ── Unknown / fallback ─────────────────────────────────────────────────────
118
+ console.log(' Framework not detected automatically.')
119
+ console.log(manualSnippet(domainId))
120
+ }
121
+
122
+ module.exports = { runSetup }
@@ -0,0 +1,75 @@
1
+ 'use strict'
2
+
3
+ const BOT_PATTERN = 'GPTBot|ClaudeBot|anthropic-ai|PerplexityBot|Google-Extended|ChatGPT-User'
4
+
5
+ /**
6
+ * The 4-line tracking block injected into existing middleware files.
7
+ * Indented with 2 spaces to match typical Next.js middleware style.
8
+ */
9
+ function trackingBlock(indent = ' ') {
10
+ return [
11
+ `${indent}// Visibly bot tracking — added by npx visibly setup`,
12
+ `${indent}const _vua = request.headers.get('user-agent') ?? ''`,
13
+ `${indent}if (/${BOT_PATTERN}/.test(_vua))`,
14
+ `${indent} fetch(\`https://visibly.io/api/bot-visit/\${process.env.VISIBLY_DOMAIN_ID}\`, { method: 'POST', headers: { 'user-agent': _vua } })`,
15
+ `${indent}// end Visibly`,
16
+ ].join('\n')
17
+ }
18
+
19
+ /**
20
+ * Full middleware.ts template — used when no middleware.ts exists yet.
21
+ */
22
+ function nextMiddlewareTemplate(domainId) {
23
+ return `import { NextRequest, NextResponse } from 'next/server'
24
+
25
+ export function middleware(request: NextRequest) {
26
+ // Visibly bot tracking — added by npx visibly setup
27
+ const _vua = request.headers.get('user-agent') ?? ''
28
+ if (/${BOT_PATTERN}/.test(_vua))
29
+ fetch(\`https://visibly.io/api/bot-visit/${domainId}\`, { method: 'POST', headers: { 'user-agent': _vua } })
30
+ // end Visibly
31
+
32
+ return NextResponse.next()
33
+ }
34
+
35
+ export const config = {
36
+ matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
37
+ }
38
+ `
39
+ }
40
+
41
+ /**
42
+ * Express middleware block injected after app initialisation.
43
+ */
44
+ function expressBlock() {
45
+ return `
46
+ // Visibly bot tracking — added by npx visibly setup
47
+ app.use((req, _res, next) => {
48
+ const _vua = req.headers['user-agent'] || ''
49
+ if (/${BOT_PATTERN}/.test(_vua))
50
+ fetch(\`https://visibly.io/api/bot-visit/\${process.env.VISIBLY_DOMAIN_ID}\`, { method: 'POST', headers: { 'user-agent': _vua } })
51
+ next()
52
+ })
53
+ // end Visibly`
54
+ }
55
+
56
+ /**
57
+ * Manual snippet printed when framework is not detected.
58
+ */
59
+ function manualSnippet(domainId) {
60
+ return `
61
+ Framework not detected automatically. Add this to your server's request handler:
62
+
63
+ // Fire-and-forget — no await, no latency impact
64
+ const ua = <incoming user-agent header>
65
+ if (/${BOT_PATTERN}/.test(ua))
66
+ fetch('https://visibly.io/api/bot-visit/${domainId}', {
67
+ method: 'POST',
68
+ headers: { 'user-agent': ua }
69
+ })
70
+
71
+ Your domain ID is already embedded above — no further configuration needed.
72
+ `
73
+ }
74
+
75
+ module.exports = { trackingBlock, nextMiddlewareTemplate, expressBlock, manualSnippet }