archer-wizard 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/.env.example +6 -0
- package/README.md +140 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +77 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/ascii.d.ts +14 -0
- package/dist/lib/ascii.d.ts.map +1 -0
- package/dist/lib/ascii.js +88 -0
- package/dist/lib/ascii.js.map +1 -0
- package/dist/lib/supabase.d.ts +7 -0
- package/dist/lib/supabase.d.ts.map +1 -0
- package/dist/lib/supabase.js +54 -0
- package/dist/lib/supabase.js.map +1 -0
- package/dist/lib/webhook.d.ts +4 -0
- package/dist/lib/webhook.d.ts.map +1 -0
- package/dist/lib/webhook.js +53 -0
- package/dist/lib/webhook.js.map +1 -0
- package/dist/tools/watch.d.ts +35 -0
- package/dist/tools/watch.d.ts.map +1 -0
- package/dist/tools/watch.js +178 -0
- package/dist/tools/watch.js.map +1 -0
- package/dist/types/index.d.ts +84 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +24 -0
- package/dist/types/index.js.map +1 -0
- package/dist/wizard/detector.d.ts +5 -0
- package/dist/wizard/detector.d.ts.map +1 -0
- package/dist/wizard/detector.js +125 -0
- package/dist/wizard/detector.js.map +1 -0
- package/dist/wizard/index.d.ts +2 -0
- package/dist/wizard/index.d.ts.map +1 -0
- package/dist/wizard/index.js +50 -0
- package/dist/wizard/index.js.map +1 -0
- package/dist/wizard/injector.d.ts +3 -0
- package/dist/wizard/injector.d.ts.map +1 -0
- package/dist/wizard/injector.js +91 -0
- package/dist/wizard/injector.js.map +1 -0
- package/dist/wizard/rules.d.ts +3 -0
- package/dist/wizard/rules.d.ts.map +1 -0
- package/dist/wizard/rules.js +94 -0
- package/dist/wizard/rules.js.map +1 -0
- package/dist/wizard/scanner.d.ts +7 -0
- package/dist/wizard/scanner.d.ts.map +1 -0
- package/dist/wizard/scanner.js +201 -0
- package/dist/wizard/scanner.js.map +1 -0
- package/package.json +28 -0
- package/src/index.ts +90 -0
- package/src/lib/ascii.ts +109 -0
- package/src/lib/supabase.ts +78 -0
- package/src/lib/webhook.ts +69 -0
- package/src/tools/watch.ts +223 -0
- package/src/types/index.ts +120 -0
- package/src/wizard/detector.ts +151 -0
- package/src/wizard/index.ts +69 -0
- package/src/wizard/injector.ts +115 -0
- package/src/wizard/rules.ts +112 -0
- package/src/wizard/scanner.ts +250 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import * as clack from '@clack/prompts';
|
|
4
|
+
import pc from 'picocolors';
|
|
5
|
+
import { logSuccess, logError, logAction } from '../lib/ascii.js';
|
|
6
|
+
import { getConfigKey } from './detector.js';
|
|
7
|
+
import type { AgentInfo, InjectionResult, McpServerEntry } from '../types/index.js';
|
|
8
|
+
|
|
9
|
+
// ─── Archer MCP Entry ───────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
function buildArcherEntry(supabaseUrl: string, serviceRoleKey: string): McpServerEntry {
|
|
12
|
+
return {
|
|
13
|
+
command: 'npx',
|
|
14
|
+
args: ['-y', 'archer', '--mcp'],
|
|
15
|
+
env: {
|
|
16
|
+
SUPABASE_URL: supabaseUrl,
|
|
17
|
+
SUPABASE_SERVICE_ROLE_KEY: serviceRoleKey,
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// ─── Read or Create Config ──────────────────────────────────
|
|
23
|
+
|
|
24
|
+
function readConfig(configPath: string): Record<string, unknown> {
|
|
25
|
+
try {
|
|
26
|
+
if (fs.existsSync(configPath)) {
|
|
27
|
+
const content = fs.readFileSync(configPath, 'utf-8');
|
|
28
|
+
return JSON.parse(content) as Record<string, unknown>;
|
|
29
|
+
}
|
|
30
|
+
} catch {
|
|
31
|
+
// Parse error or read error — start fresh
|
|
32
|
+
}
|
|
33
|
+
return {};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function writeConfig(configPath: string, config: Record<string, unknown>): void {
|
|
37
|
+
const dir = path.dirname(configPath);
|
|
38
|
+
if (!fs.existsSync(dir)) {
|
|
39
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
40
|
+
}
|
|
41
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ─── Inject Into Single Agent ───────────────────────────────
|
|
45
|
+
|
|
46
|
+
function injectIntoAgent(
|
|
47
|
+
agent: AgentInfo,
|
|
48
|
+
supabaseUrl: string,
|
|
49
|
+
serviceRoleKey: string,
|
|
50
|
+
): InjectionResult {
|
|
51
|
+
try {
|
|
52
|
+
const configKey = getConfigKey(agent.name);
|
|
53
|
+
const config = readConfig(agent.configPath);
|
|
54
|
+
const archerEntry = buildArcherEntry(supabaseUrl, serviceRoleKey);
|
|
55
|
+
|
|
56
|
+
// Ensure the config key object exists
|
|
57
|
+
if (!config[configKey] || typeof config[configKey] !== 'object') {
|
|
58
|
+
config[configKey] = {};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Inject archer entry without overwriting other servers
|
|
62
|
+
const servers = config[configKey] as Record<string, unknown>;
|
|
63
|
+
servers['archer'] = archerEntry;
|
|
64
|
+
|
|
65
|
+
writeConfig(agent.configPath, config);
|
|
66
|
+
|
|
67
|
+
logSuccess(`injected into ${pc.bold(agent.name)} → ${agent.configPath}`);
|
|
68
|
+
return { agent: agent.name, success: true };
|
|
69
|
+
} catch (err) {
|
|
70
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
71
|
+
logError(`failed to inject into ${agent.name}: ${message}`);
|
|
72
|
+
return { agent: agent.name, success: false, error: message };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ─── Main Injector ──────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
export async function injectIntoAgents(
|
|
79
|
+
agents: AgentInfo[],
|
|
80
|
+
supabaseUrl: string,
|
|
81
|
+
serviceRoleKey: string,
|
|
82
|
+
): Promise<InjectionResult[]> {
|
|
83
|
+
if (agents.length === 0) {
|
|
84
|
+
logError('no AI agents detected on this system');
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Display found agents
|
|
89
|
+
console.log();
|
|
90
|
+
logAction('detected agents:');
|
|
91
|
+
for (const agent of agents) {
|
|
92
|
+
console.log(` ${pc.dim('·')} ${pc.bold(agent.name)}`);
|
|
93
|
+
}
|
|
94
|
+
console.log();
|
|
95
|
+
|
|
96
|
+
// Single confirmation
|
|
97
|
+
const shouldInject = await clack.confirm({
|
|
98
|
+
message: `inject archer into ${agents.length} agent${agents.length !== 1 ? 's' : ''}?`,
|
|
99
|
+
initialValue: true,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
if (clack.isCancel(shouldInject) || !shouldInject) {
|
|
103
|
+
logAction('skipped injection');
|
|
104
|
+
return [];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Inject into all confirmed agents
|
|
108
|
+
const results: InjectionResult[] = [];
|
|
109
|
+
for (const agent of agents) {
|
|
110
|
+
const result = injectIntoAgent(agent, supabaseUrl, serviceRoleKey);
|
|
111
|
+
results.push(result);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return results;
|
|
115
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { logSuccess, logError } from '../lib/ascii.js';
|
|
4
|
+
import { getRulesPath } from './detector.js';
|
|
5
|
+
import type { AgentInfo } from '../types/index.js';
|
|
6
|
+
|
|
7
|
+
// ─── Rule Content ───────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
const RULE_CONTENT = `
|
|
10
|
+
# Archer — Event Intelligence for AI Agents
|
|
11
|
+
|
|
12
|
+
Archer is connected to this project's Supabase instance.
|
|
13
|
+
|
|
14
|
+
## Available Tool: archer.watch
|
|
15
|
+
|
|
16
|
+
You can watch real-time events from Supabase using the \`archer.watch\` MCP tool.
|
|
17
|
+
|
|
18
|
+
### Parameters
|
|
19
|
+
|
|
20
|
+
- **source**: always \`"supabase"\`
|
|
21
|
+
- **event**: one of \`"auth.signup"\`, \`"table.insert"\`, \`"table.update"\`, \`"table.delete"\`
|
|
22
|
+
- **table**: the table name (required for table events)
|
|
23
|
+
- **condition**: optional filter like \`"email ends with @gmail.com"\`
|
|
24
|
+
- **webhookUrl**: URL to receive POST notifications
|
|
25
|
+
|
|
26
|
+
### Examples
|
|
27
|
+
|
|
28
|
+
1. Watch for new user signups:
|
|
29
|
+
\`\`\`
|
|
30
|
+
archer.watch({ source: "supabase", event: "auth.signup", webhookUrl: "https://hooks.example.com/signups" })
|
|
31
|
+
\`\`\`
|
|
32
|
+
|
|
33
|
+
2. Watch for inserts to the orders table:
|
|
34
|
+
\`\`\`
|
|
35
|
+
archer.watch({ source: "supabase", event: "table.insert", table: "orders", webhookUrl: "https://hooks.example.com/orders" })
|
|
36
|
+
\`\`\`
|
|
37
|
+
|
|
38
|
+
3. Watch with a condition:
|
|
39
|
+
\`\`\`
|
|
40
|
+
archer.watch({ source: "supabase", event: "table.insert", table: "users", condition: "email ends with @company.com", webhookUrl: "https://hooks.example.com/vip" })
|
|
41
|
+
\`\`\`
|
|
42
|
+
|
|
43
|
+
When the user asks to "watch" or "monitor" something in their database, use this tool.
|
|
44
|
+
`.trim();
|
|
45
|
+
|
|
46
|
+
// ─── Archer Block Markers ───────────────────────────────────
|
|
47
|
+
|
|
48
|
+
const BLOCK_START = '<!-- archer:start -->';
|
|
49
|
+
const BLOCK_END = '<!-- archer:end -->';
|
|
50
|
+
|
|
51
|
+
function wrapInBlock(content: string): string {
|
|
52
|
+
return `${BLOCK_START}\n${content}\n${BLOCK_END}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ─── Replace or Append Block ────────────────────────────────
|
|
56
|
+
|
|
57
|
+
function insertContent(existingContent: string, newBlock: string): string {
|
|
58
|
+
const startIdx = existingContent.indexOf(BLOCK_START);
|
|
59
|
+
const endIdx = existingContent.indexOf(BLOCK_END);
|
|
60
|
+
|
|
61
|
+
if (startIdx !== -1 && endIdx !== -1) {
|
|
62
|
+
// Replace existing block
|
|
63
|
+
return (
|
|
64
|
+
existingContent.slice(0, startIdx) +
|
|
65
|
+
newBlock +
|
|
66
|
+
existingContent.slice(endIdx + BLOCK_END.length)
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Append to end
|
|
71
|
+
const separator = existingContent.endsWith('\n') ? '\n' : '\n\n';
|
|
72
|
+
return existingContent + separator + newBlock + '\n';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ─── Write Rules to Agent ───────────────────────────────────
|
|
76
|
+
|
|
77
|
+
function writeRulesForAgent(agent: AgentInfo, cwd: string): boolean {
|
|
78
|
+
try {
|
|
79
|
+
const rulesPath = getRulesPath(agent.name, cwd);
|
|
80
|
+
const dir = path.dirname(rulesPath);
|
|
81
|
+
const wrappedContent = wrapInBlock(RULE_CONTENT);
|
|
82
|
+
|
|
83
|
+
// Ensure directory exists
|
|
84
|
+
if (!fs.existsSync(dir)) {
|
|
85
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Read existing content or start fresh
|
|
89
|
+
let existingContent = '';
|
|
90
|
+
if (fs.existsSync(rulesPath)) {
|
|
91
|
+
existingContent = fs.readFileSync(rulesPath, 'utf-8');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const finalContent = insertContent(existingContent, wrappedContent);
|
|
95
|
+
fs.writeFileSync(rulesPath, finalContent, 'utf-8');
|
|
96
|
+
|
|
97
|
+
logSuccess(`wrote rules to ${agent.name} → ${rulesPath}`);
|
|
98
|
+
return true;
|
|
99
|
+
} catch (err) {
|
|
100
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
101
|
+
logError(`failed to write rules for ${agent.name}: ${message}`);
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ─── Main Rules Injector ────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
export function injectRules(agents: AgentInfo[], cwd: string): void {
|
|
109
|
+
for (const agent of agents) {
|
|
110
|
+
writeRulesForAgent(agent, cwd);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import * as clack from '@clack/prompts';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { logSuccess, logError, maskCredential } from '../lib/ascii.js';
|
|
6
|
+
import type { ScanResult, Framework } from '../types/index.js';
|
|
7
|
+
|
|
8
|
+
// ─── Env File Priority ──────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
const ENV_FILES = [
|
|
11
|
+
'.env.local',
|
|
12
|
+
'.env',
|
|
13
|
+
'.env.development',
|
|
14
|
+
'.env.production',
|
|
15
|
+
] as const;
|
|
16
|
+
|
|
17
|
+
// ─── Key Aliases ────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
const SUPABASE_URL_ALIASES = [
|
|
20
|
+
'SUPABASE_URL',
|
|
21
|
+
'NEXT_PUBLIC_SUPABASE_URL',
|
|
22
|
+
'VITE_SUPABASE_URL',
|
|
23
|
+
] as const;
|
|
24
|
+
|
|
25
|
+
const SERVICE_ROLE_KEY_ALIASES = [
|
|
26
|
+
'SUPABASE_SERVICE_ROLE_KEY',
|
|
27
|
+
'SUPABASE_SERVICE_KEY',
|
|
28
|
+
] as const;
|
|
29
|
+
|
|
30
|
+
const ANON_KEY_ALIASES = [
|
|
31
|
+
'SUPABASE_ANON_KEY',
|
|
32
|
+
'NEXT_PUBLIC_SUPABASE_ANON_KEY',
|
|
33
|
+
'VITE_SUPABASE_ANON_KEY',
|
|
34
|
+
] as const;
|
|
35
|
+
|
|
36
|
+
// ─── Validation Schemas ─────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
const SupabaseUrlSchema = z.string().url().startsWith('https://', {
|
|
39
|
+
message: 'SUPABASE_URL must start with https://',
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const ServiceRoleKeySchema = z.string().min(1, {
|
|
43
|
+
message: 'SUPABASE_SERVICE_ROLE_KEY must be a non-empty string',
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// ─── Parse Env File ─────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
function parseEnvFile(filePath: string): Map<string, string> {
|
|
49
|
+
const vars = new Map<string, string>();
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
53
|
+
const lines = content.split('\n');
|
|
54
|
+
|
|
55
|
+
for (const rawLine of lines) {
|
|
56
|
+
const line = rawLine.trim();
|
|
57
|
+
if (!line || line.startsWith('#')) continue;
|
|
58
|
+
|
|
59
|
+
const eqIndex = line.indexOf('=');
|
|
60
|
+
if (eqIndex === -1) continue;
|
|
61
|
+
|
|
62
|
+
const key = line.slice(0, eqIndex).trim();
|
|
63
|
+
let value = line.slice(eqIndex + 1).trim();
|
|
64
|
+
|
|
65
|
+
// Strip surrounding quotes
|
|
66
|
+
if (
|
|
67
|
+
(value.startsWith('"') && value.endsWith('"')) ||
|
|
68
|
+
(value.startsWith("'") && value.endsWith("'"))
|
|
69
|
+
) {
|
|
70
|
+
value = value.slice(1, -1);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (key && value) {
|
|
74
|
+
vars.set(key, value);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
} catch {
|
|
78
|
+
// File doesn't exist or can't be read — skip silently
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return vars;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ─── Find Key By Aliases ────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
function findByAliases(
|
|
87
|
+
envMap: Map<string, string>,
|
|
88
|
+
aliases: readonly string[],
|
|
89
|
+
): string | null {
|
|
90
|
+
for (const alias of aliases) {
|
|
91
|
+
const value = envMap.get(alias);
|
|
92
|
+
if (value) return value;
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ─── Detect Framework ───────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
function detectFramework(cwd: string): { framework: Framework; hasSupabaseInstalled: boolean } {
|
|
100
|
+
let framework: Framework = 'unknown';
|
|
101
|
+
let hasSupabaseInstalled = false;
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
const pkgPath = path.join(cwd, 'package.json');
|
|
105
|
+
const content = fs.readFileSync(pkgPath, 'utf-8');
|
|
106
|
+
const pkg = JSON.parse(content) as {
|
|
107
|
+
dependencies?: Record<string, string>;
|
|
108
|
+
devDependencies?: Record<string, string>;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const allDeps = {
|
|
112
|
+
...pkg.dependencies,
|
|
113
|
+
...pkg.devDependencies,
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
if ('next' in allDeps) {
|
|
117
|
+
framework = 'nextjs';
|
|
118
|
+
} else if ('vite' in allDeps) {
|
|
119
|
+
framework = 'vite';
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if ('@supabase/supabase-js' in allDeps) {
|
|
123
|
+
hasSupabaseInstalled = true;
|
|
124
|
+
}
|
|
125
|
+
} catch {
|
|
126
|
+
// No package.json or parse error — defaults are fine
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return { framework, hasSupabaseInstalled };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ─── Main Scanner ───────────────────────────────────────────
|
|
133
|
+
|
|
134
|
+
export async function scanProject(cwd: string): Promise<ScanResult> {
|
|
135
|
+
// Merge all env files (earlier files win)
|
|
136
|
+
const mergedEnv = new Map<string, string>();
|
|
137
|
+
let foundInFile: string | null = null;
|
|
138
|
+
|
|
139
|
+
// Read in reverse so earlier files overwrite later ones
|
|
140
|
+
for (const file of [...ENV_FILES].reverse()) {
|
|
141
|
+
const filePath = path.join(cwd, file);
|
|
142
|
+
const vars = parseEnvFile(filePath);
|
|
143
|
+
for (const [key, value] of vars) {
|
|
144
|
+
mergedEnv.set(key, value);
|
|
145
|
+
}
|
|
146
|
+
if (vars.size > 0 && !foundInFile) {
|
|
147
|
+
foundInFile = file;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Re-check which file had the first match (priority order)
|
|
152
|
+
for (const file of ENV_FILES) {
|
|
153
|
+
const filePath = path.join(cwd, file);
|
|
154
|
+
const vars = parseEnvFile(filePath);
|
|
155
|
+
if (vars.size > 0) {
|
|
156
|
+
foundInFile = file;
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const supabaseUrl = findByAliases(mergedEnv, SUPABASE_URL_ALIASES);
|
|
162
|
+
const serviceRoleKey = findByAliases(mergedEnv, SERVICE_ROLE_KEY_ALIASES);
|
|
163
|
+
const anonKey = findByAliases(mergedEnv, ANON_KEY_ALIASES);
|
|
164
|
+
|
|
165
|
+
const { framework, hasSupabaseInstalled } = detectFramework(cwd);
|
|
166
|
+
|
|
167
|
+
// Log found credentials
|
|
168
|
+
if (supabaseUrl) {
|
|
169
|
+
logSuccess(`SUPABASE_URL = ${maskCredential(supabaseUrl)}`);
|
|
170
|
+
} else {
|
|
171
|
+
logError('missing SUPABASE_URL');
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (serviceRoleKey) {
|
|
175
|
+
logSuccess(`SUPABASE_SERVICE_ROLE_KEY = ${maskCredential(serviceRoleKey)}`);
|
|
176
|
+
} else {
|
|
177
|
+
logError('missing SUPABASE_SERVICE_ROLE_KEY');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (anonKey) {
|
|
181
|
+
logSuccess(`SUPABASE_ANON_KEY = ${maskCredential(anonKey)}`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (foundInFile) {
|
|
185
|
+
logSuccess(`found credentials in ${foundInFile}`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
supabaseUrl,
|
|
190
|
+
serviceRoleKey,
|
|
191
|
+
anonKey,
|
|
192
|
+
framework,
|
|
193
|
+
hasSupabaseInstalled,
|
|
194
|
+
foundInFile,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ─── Prompt for Missing Credentials ─────────────────────────
|
|
199
|
+
|
|
200
|
+
export async function promptForMissing(
|
|
201
|
+
scan: ScanResult,
|
|
202
|
+
): Promise<{ supabaseUrl: string; serviceRoleKey: string }> {
|
|
203
|
+
let supabaseUrl = scan.supabaseUrl;
|
|
204
|
+
let serviceRoleKey = scan.serviceRoleKey;
|
|
205
|
+
|
|
206
|
+
if (!supabaseUrl) {
|
|
207
|
+
const value = await clack.text({
|
|
208
|
+
message: 'Enter your SUPABASE_URL',
|
|
209
|
+
placeholder: 'https://your-project.supabase.co',
|
|
210
|
+
validate: (input) => {
|
|
211
|
+
const result = SupabaseUrlSchema.safeParse(input);
|
|
212
|
+
if (!result.success) {
|
|
213
|
+
return result.error.issues[0]?.message ?? 'Invalid URL';
|
|
214
|
+
}
|
|
215
|
+
return undefined;
|
|
216
|
+
},
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
if (clack.isCancel(value)) {
|
|
220
|
+
process.exit(0);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
supabaseUrl = value as string;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (!serviceRoleKey) {
|
|
227
|
+
const value = await clack.text({
|
|
228
|
+
message: 'Enter your SUPABASE_SERVICE_ROLE_KEY',
|
|
229
|
+
placeholder: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
|
|
230
|
+
validate: (input) => {
|
|
231
|
+
const result = ServiceRoleKeySchema.safeParse(input);
|
|
232
|
+
if (!result.success) {
|
|
233
|
+
return result.error.issues[0]?.message ?? 'Invalid key';
|
|
234
|
+
}
|
|
235
|
+
return undefined;
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
if (clack.isCancel(value)) {
|
|
240
|
+
process.exit(0);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
serviceRoleKey = value as string;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return {
|
|
247
|
+
supabaseUrl: SupabaseUrlSchema.parse(supabaseUrl),
|
|
248
|
+
serviceRoleKey: ServiceRoleKeySchema.parse(serviceRoleKey),
|
|
249
|
+
};
|
|
250
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"outDir": "dist",
|
|
7
|
+
"rootDir": "src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"noImplicitAny": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"forceConsistentCasingInFileNames": true,
|
|
13
|
+
"resolveJsonModule": true,
|
|
14
|
+
"declaration": true,
|
|
15
|
+
"declarationMap": true,
|
|
16
|
+
"sourceMap": true
|
|
17
|
+
},
|
|
18
|
+
"include": ["src/**/*"],
|
|
19
|
+
"exclude": ["node_modules", "dist"]
|
|
20
|
+
}
|