@contextai-core/cli 0.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 +14 -0
- package/index.js +112 -0
- package/package.json +31 -0
- package/src/actions/auth.js +149 -0
- package/src/actions/doctor.js +73 -0
- package/src/actions/init.js +62 -0
- package/src/actions/install.js +268 -0
- package/src/actions/publish.js +42 -0
- package/src/utils/fs.js +27 -0
- package/src/utils/merge.js +97 -0
- package/src/utils/supabase.js +37 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
ContextAI CLI - Proprietary License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 ContextAI
|
|
4
|
+
|
|
5
|
+
All rights reserved.
|
|
6
|
+
|
|
7
|
+
This software and associated documentation files (the "Software") are proprietary
|
|
8
|
+
and confidential. Unauthorized copying, distribution, modification, public display,
|
|
9
|
+
or public performance of this Software is strictly prohibited.
|
|
10
|
+
|
|
11
|
+
The Software is provided for use solely by authorized users who have agreed to
|
|
12
|
+
the ContextAI Terms of Service.
|
|
13
|
+
|
|
14
|
+
For licensing inquiries, contact: support@contextai.in
|
package/index.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import figlet from 'figlet';
|
|
6
|
+
import fs from 'fs';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
|
|
9
|
+
// Actions
|
|
10
|
+
import { login, logout, whoami } from './src/actions/auth.js';
|
|
11
|
+
import { init } from './src/actions/init.js';
|
|
12
|
+
import { publish } from './src/actions/publish.js';
|
|
13
|
+
import { installPackage } from './src/actions/install.js';
|
|
14
|
+
import { doctor } from './src/actions/doctor.js';
|
|
15
|
+
|
|
16
|
+
// Helper to load env from local .env for testing
|
|
17
|
+
if (fs.existsSync('.env')) {
|
|
18
|
+
const envConfig = fs.readFileSync('.env', 'utf8');
|
|
19
|
+
envConfig.split('\n').forEach(line => {
|
|
20
|
+
const [key, value] = line.split('=');
|
|
21
|
+
if (key && value) {
|
|
22
|
+
process.env[key.trim()] = value.trim();
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const program = new Command();
|
|
28
|
+
|
|
29
|
+
console.log(
|
|
30
|
+
chalk.blue(
|
|
31
|
+
figlet.textSync('ContextAI', { horizontalLayout: 'full' })
|
|
32
|
+
)
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
program
|
|
36
|
+
.version('1.0.0')
|
|
37
|
+
.description('ContextAI CLI - The npm for AI Context');
|
|
38
|
+
|
|
39
|
+
// AUTH COMMANDS
|
|
40
|
+
program.command('login').description('Authenticate with ContextAI via GitHub').action(login);
|
|
41
|
+
program.command('whoami').description('Show current logged in user').action(whoami);
|
|
42
|
+
program.command('logout').description('Log out and remove saved credentials').action(logout);
|
|
43
|
+
|
|
44
|
+
// CORE COMMANDS
|
|
45
|
+
program.command('init').description('Initialize a new .ai/ context in the current directory').action(init);
|
|
46
|
+
program.command('doctor').description('Check environment for issues').action(doctor);
|
|
47
|
+
|
|
48
|
+
program
|
|
49
|
+
.command('install <package_name>')
|
|
50
|
+
.description('Install a context package (e.g., @stacks/nextjs-stack)')
|
|
51
|
+
.action(async (packageName) => {
|
|
52
|
+
await installPackage(packageName);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
program
|
|
56
|
+
.command('update <package_name>')
|
|
57
|
+
.description('Update/Re-install a context package')
|
|
58
|
+
.action(async (packageName) => {
|
|
59
|
+
await installPackage(packageName);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
program
|
|
63
|
+
.command('list')
|
|
64
|
+
.description('List installed context packages')
|
|
65
|
+
.action(() => {
|
|
66
|
+
const aiDir = path.join(process.cwd(), '.ai');
|
|
67
|
+
const manifestPath = path.join(aiDir, 'context.json');
|
|
68
|
+
|
|
69
|
+
if (!fs.existsSync(manifestPath)) {
|
|
70
|
+
console.log(chalk.yellow('No .ai/ context found. Run `contextai init` first.'));
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
76
|
+
const packages = manifest.packages || {};
|
|
77
|
+
|
|
78
|
+
console.log(chalk.bold('\nInstalled Packages:\n'));
|
|
79
|
+
|
|
80
|
+
const count = Object.keys(packages).length;
|
|
81
|
+
if (count === 0) {
|
|
82
|
+
console.log(chalk.gray(' (No packages installed)'));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
Object.keys(packages).forEach(pkgName => {
|
|
86
|
+
const pkg = packages[pkgName];
|
|
87
|
+
console.log(chalk.cyan(` ${pkgName}`));
|
|
88
|
+
console.log(chalk.gray(` Type: ${pkg.type}`));
|
|
89
|
+
console.log(chalk.gray(` Path: ${pkg.path}`));
|
|
90
|
+
if (pkg.installedAt) console.log(chalk.gray(` Installed: ${new Date(pkg.installedAt).toLocaleString()}`));
|
|
91
|
+
console.log('');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
} catch (e) {
|
|
95
|
+
console.error(chalk.red('Error reading context manifest.'));
|
|
96
|
+
console.error(e);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
program.command('publish').description('Publish a context package to the registry').action(publish);
|
|
101
|
+
|
|
102
|
+
// LEGACY COMMANDS
|
|
103
|
+
program
|
|
104
|
+
.command('pull <slug>')
|
|
105
|
+
.description('DEPRECATED: Use install')
|
|
106
|
+
.action(async (slug) => {
|
|
107
|
+
console.warn(chalk.yellow('\n⚠️ WARNING: `contextai pull` is deprecated. Please use `contextai install` instead.\n'));
|
|
108
|
+
console.log(chalk.gray(`Attempting legacy pull for ${slug} -> install...`));
|
|
109
|
+
await installPackage(slug);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
program.parse(process.argv);
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@contextai-core/cli",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "The npm for AI Context - install context packs for your AI agents",
|
|
5
|
+
"author": "ContextAI",
|
|
6
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"ai",
|
|
9
|
+
"context",
|
|
10
|
+
"llm",
|
|
11
|
+
"cli",
|
|
12
|
+
"agents",
|
|
13
|
+
"cursor",
|
|
14
|
+
"windsurf"
|
|
15
|
+
],
|
|
16
|
+
"main": "index.js",
|
|
17
|
+
"bin": {
|
|
18
|
+
"contextai": "index.js"
|
|
19
|
+
},
|
|
20
|
+
"type": "module",
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@supabase/supabase-js": "^2.89.0",
|
|
23
|
+
"adm-zip": "^0.5.16",
|
|
24
|
+
"chalk": "^5.3.0",
|
|
25
|
+
"commander": "^11.1.0",
|
|
26
|
+
"figlet": "^1.7.0",
|
|
27
|
+
"node-fetch": "^3.3.2",
|
|
28
|
+
"open": "^11.0.0",
|
|
29
|
+
"ora": "^8.0.0"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import http from 'http';
|
|
4
|
+
import url from 'url';
|
|
5
|
+
import os from 'os';
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
import open from 'open';
|
|
8
|
+
import { createClient } from '@supabase/supabase-js';
|
|
9
|
+
|
|
10
|
+
// Config file location
|
|
11
|
+
const CONFIG_DIR = path.join(os.homedir(), '.contextai');
|
|
12
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, 'credentials.json');
|
|
13
|
+
|
|
14
|
+
// Load saved credentials if they exist
|
|
15
|
+
function loadCredentials() {
|
|
16
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
17
|
+
return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
|
|
18
|
+
}
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Save credentials
|
|
23
|
+
function saveCredentials(creds) {
|
|
24
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
25
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
26
|
+
}
|
|
27
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(creds, null, 2));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function login() {
|
|
31
|
+
const supabaseUrl = process.env.VITE_SUPABASE_URL;
|
|
32
|
+
const supabaseKey = process.env.VITE_SUPABASE_ANON_KEY;
|
|
33
|
+
|
|
34
|
+
if (!supabaseUrl || !supabaseKey) {
|
|
35
|
+
console.error(chalk.red('❌ Error: Supabase URL and Key must be set.'));
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
console.log(chalk.blue('🔐 Starting authentication...'));
|
|
40
|
+
|
|
41
|
+
// Start temporary HTTP server to catch callback
|
|
42
|
+
const PORT = 3333;
|
|
43
|
+
const REDIRECT_URI = `http://localhost:${PORT}/callback`;
|
|
44
|
+
|
|
45
|
+
const server = http.createServer(async (req, res) => {
|
|
46
|
+
const reqUrl = url.parse(req.url, true);
|
|
47
|
+
|
|
48
|
+
if (reqUrl.pathname === '/callback') {
|
|
49
|
+
// Extract tokens from hash (Supabase sends in hash fragment)
|
|
50
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
51
|
+
res.end(`
|
|
52
|
+
<html>
|
|
53
|
+
<head><meta charset="utf-8"><title>ContextAI Login</title></head>
|
|
54
|
+
<body style="font-family: system-ui; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: #0a0a0a; color: white;">
|
|
55
|
+
<div style="text-align: center;">
|
|
56
|
+
<h1>✅ Login Successful!</h1>
|
|
57
|
+
<p>You can close this window and return to the terminal.</p>
|
|
58
|
+
<script>
|
|
59
|
+
// Extract hash params and send to parent
|
|
60
|
+
const hash = window.location.hash.substring(1);
|
|
61
|
+
const params = new URLSearchParams(hash);
|
|
62
|
+
const accessToken = params.get('access_token');
|
|
63
|
+
const refreshToken = params.get('refresh_token');
|
|
64
|
+
|
|
65
|
+
if (accessToken) {
|
|
66
|
+
fetch('/save-token?access_token=' + accessToken + '&refresh_token=' + (refreshToken || ''));
|
|
67
|
+
}
|
|
68
|
+
</script>
|
|
69
|
+
</div>
|
|
70
|
+
</body>
|
|
71
|
+
</html>
|
|
72
|
+
`);
|
|
73
|
+
} else if (reqUrl.pathname === '/save-token') {
|
|
74
|
+
const accessToken = reqUrl.query.access_token;
|
|
75
|
+
const refreshToken = reqUrl.query.refresh_token;
|
|
76
|
+
|
|
77
|
+
if (accessToken) {
|
|
78
|
+
// Get user info
|
|
79
|
+
const supabase = createClient(supabaseUrl, supabaseKey);
|
|
80
|
+
const { data: { user }, error } = await supabase.auth.getUser(accessToken);
|
|
81
|
+
|
|
82
|
+
if (user && !error) {
|
|
83
|
+
saveCredentials({
|
|
84
|
+
access_token: accessToken,
|
|
85
|
+
refresh_token: refreshToken,
|
|
86
|
+
user_id: user.id,
|
|
87
|
+
email: user.email,
|
|
88
|
+
name: user.user_metadata?.full_name || user.email
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
console.log(chalk.green(`\n✅ Logged in as ${user.user_metadata?.full_name || user.email}!`));
|
|
92
|
+
console.log(chalk.gray(`Credentials saved to ${CONFIG_FILE}`));
|
|
93
|
+
} else {
|
|
94
|
+
console.error(chalk.red('❌ Failed to get user info'));
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
res.writeHead(200);
|
|
99
|
+
res.end('ok');
|
|
100
|
+
|
|
101
|
+
// Close server after a short delay
|
|
102
|
+
setTimeout(() => {
|
|
103
|
+
server.close();
|
|
104
|
+
process.exit(0);
|
|
105
|
+
}, 500);
|
|
106
|
+
} else {
|
|
107
|
+
res.writeHead(404);
|
|
108
|
+
res.end('Not found');
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
server.listen(PORT, async () => {
|
|
113
|
+
console.log(chalk.gray(`Callback server started on port ${PORT}`));
|
|
114
|
+
|
|
115
|
+
// Build OAuth URL
|
|
116
|
+
const authUrl = `${supabaseUrl}/auth/v1/authorize?provider=github&redirect_to=${encodeURIComponent(REDIRECT_URI)}`;
|
|
117
|
+
|
|
118
|
+
console.log(chalk.yellow('\n📱 Opening browser for GitHub login...'));
|
|
119
|
+
console.log(chalk.gray(`If browser doesn't open, visit: ${authUrl}\n`));
|
|
120
|
+
|
|
121
|
+
await open(authUrl);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// Timeout after 2 minutes
|
|
125
|
+
setTimeout(() => {
|
|
126
|
+
console.log(chalk.red('\n❌ Login timed out. Please try again.'));
|
|
127
|
+
server.close();
|
|
128
|
+
process.exit(1);
|
|
129
|
+
}, 120000);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function whoami() {
|
|
133
|
+
const creds = loadCredentials();
|
|
134
|
+
if (creds) {
|
|
135
|
+
console.log(chalk.green(`Logged in as: ${creds.name || creds.email}`));
|
|
136
|
+
console.log(chalk.gray(`User ID: ${creds.user_id}`));
|
|
137
|
+
} else {
|
|
138
|
+
console.log(chalk.yellow('Not logged in. Run `contextai login` first.'));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function logout() {
|
|
143
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
144
|
+
fs.unlinkSync(CONFIG_FILE);
|
|
145
|
+
console.log(chalk.green('✅ Logged out successfully.'));
|
|
146
|
+
} else {
|
|
147
|
+
console.log(chalk.yellow('Already logged out.'));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { execSync } from 'child_process';
|
|
6
|
+
import fetch from 'node-fetch';
|
|
7
|
+
|
|
8
|
+
export async function doctor() {
|
|
9
|
+
console.log(chalk.bold('\n🏥 ContextAI Doctor\n'));
|
|
10
|
+
|
|
11
|
+
const checks = [
|
|
12
|
+
{
|
|
13
|
+
name: 'OS Info',
|
|
14
|
+
run: () => `${os.type()} ${os.release()} (${os.arch()})`,
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
name: 'Node.js Version',
|
|
18
|
+
run: () => {
|
|
19
|
+
const version = process.version;
|
|
20
|
+
const major = parseInt(version.substring(1).split('.')[0], 10);
|
|
21
|
+
if (major < 18) throw new Error(`Node.js ${version} is too old. Please upgrade to v18+.`);
|
|
22
|
+
return version;
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: 'Git Installed',
|
|
27
|
+
run: () => {
|
|
28
|
+
try {
|
|
29
|
+
const gitVer = execSync('git --version').toString().trim();
|
|
30
|
+
return gitVer;
|
|
31
|
+
} catch (e) {
|
|
32
|
+
throw new Error('Git is not installed or not in PATH.');
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
name: 'Context Directory (.ai)',
|
|
38
|
+
run: () => {
|
|
39
|
+
const contextPath = path.join(process.cwd(), '.ai');
|
|
40
|
+
if (fs.existsSync(contextPath)) {
|
|
41
|
+
return 'Found ✅';
|
|
42
|
+
} else {
|
|
43
|
+
return 'Not found (Run `contextai init` to create one)';
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: 'Internet Connection',
|
|
49
|
+
run: async () => {
|
|
50
|
+
try {
|
|
51
|
+
// simple fetch to google or registry
|
|
52
|
+
await fetch('https://www.google.com', { method: 'HEAD' });
|
|
53
|
+
return 'Online ✅';
|
|
54
|
+
} catch (e) {
|
|
55
|
+
throw new Error('Offline ❌');
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
for (const check of checks) {
|
|
62
|
+
process.stdout.write(`• Checking ${check.name}... `);
|
|
63
|
+
try {
|
|
64
|
+
const result = await check.run();
|
|
65
|
+
console.log(chalk.green(result));
|
|
66
|
+
} catch (error) {
|
|
67
|
+
console.log(chalk.red('FAILED'));
|
|
68
|
+
console.log(chalk.red(` User Action: ${error.message}`));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
console.log(chalk.gray('\nDiagnostic check complete.\n'));
|
|
73
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
|
|
5
|
+
export function init() {
|
|
6
|
+
console.log(chalk.green('Initializing Shared Brain...'));
|
|
7
|
+
const aiDir = path.join(process.cwd(), '.ai');
|
|
8
|
+
const ucpDir = path.join(aiDir, 'ucp');
|
|
9
|
+
const manifestPath = path.join(aiDir, 'context.json');
|
|
10
|
+
const bootPath = path.join(aiDir, 'boot.md');
|
|
11
|
+
|
|
12
|
+
if (fs.existsSync(aiDir)) {
|
|
13
|
+
// Check if it's legacy
|
|
14
|
+
if (!fs.existsSync(ucpDir) && fs.existsSync(path.join(aiDir, 'context'))) {
|
|
15
|
+
console.log(chalk.yellow('⚠️ Legacy .ai/ structure detected. Migration is recommended but not yet auto-implemented.'));
|
|
16
|
+
console.log(chalk.yellow(' Please start fresh or manually move contents to .ai/ucp/'));
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (fs.existsSync(manifestPath)) {
|
|
21
|
+
console.log(chalk.yellow('⚠️ .ai/ already initialized.'));
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// 1. Create Directories (Namespace Isolation)
|
|
27
|
+
fs.mkdirSync(aiDir, { recursive: true });
|
|
28
|
+
fs.mkdirSync(path.join(ucpDir, 'context'), { recursive: true });
|
|
29
|
+
|
|
30
|
+
// 2. Create Traffic Cop (boot.md)
|
|
31
|
+
fs.writeFileSync(
|
|
32
|
+
bootPath,
|
|
33
|
+
`# AI Context Container\n\nThis project uses multiple context protocols.\n\n## Active Protocols\n1. **UCP** (Core OS): [Unified Context Protocol](./ucp/README.md)\n - Use for: General Project Context, Workflows.\n`
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
// 3. Create Manifest (context.json)
|
|
37
|
+
const manifest = {
|
|
38
|
+
version: "1.0.0",
|
|
39
|
+
packages: {
|
|
40
|
+
"ucp": {
|
|
41
|
+
"type": "core",
|
|
42
|
+
"path": "./ucp"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
|
47
|
+
|
|
48
|
+
// 4. Seed UCP Basic Files
|
|
49
|
+
fs.writeFileSync(
|
|
50
|
+
path.join(ucpDir, 'README.md'),
|
|
51
|
+
'# Unified Context Protocol (UCP)\n\nThis directory contains the core operating system for your AI agent.'
|
|
52
|
+
);
|
|
53
|
+
fs.writeFileSync(
|
|
54
|
+
path.join(ucpDir, 'context', 'MASTER.md'),
|
|
55
|
+
'# Project Master Context\n\nPrimary context definition.'
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
console.log(chalk.green('✅ Successfully initialized Shared Brain (.ai/)!'));
|
|
59
|
+
console.log(chalk.gray(' - .ai/boot.md (Traffic Cop)'));
|
|
60
|
+
console.log(chalk.gray(' - .ai/context.json (Manifest)'));
|
|
61
|
+
console.log(chalk.gray(' - .ai/ucp/ (Core Protocol)'));
|
|
62
|
+
}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import fetch from 'node-fetch';
|
|
5
|
+
import AdmZip from 'adm-zip';
|
|
6
|
+
import os from 'os';
|
|
7
|
+
import { copyRecursiveSync } from '../utils/fs.js';
|
|
8
|
+
import ora from 'ora';
|
|
9
|
+
import { supabase } from '../utils/supabase.js';
|
|
10
|
+
|
|
11
|
+
// Load saved credentials
|
|
12
|
+
function loadCredentials() {
|
|
13
|
+
const CONFIG_FILE = path.join(os.homedir(), '.contextai', 'credentials.json');
|
|
14
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
15
|
+
return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
|
|
16
|
+
}
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function installPackage(packageSlug, options = {}) {
|
|
21
|
+
const spinner = ora(`Installing ${packageSlug}...`).start();
|
|
22
|
+
|
|
23
|
+
const aiDir = path.join(process.cwd(), '.ai');
|
|
24
|
+
const manifestPath = path.join(aiDir, 'context.json');
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
// Check .ai exists
|
|
28
|
+
if (!fs.existsSync(manifestPath)) {
|
|
29
|
+
spinner.fail('.ai/ directory not found. Run `contextai init` first.');
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// 1. Fetch pack from Supabase
|
|
34
|
+
spinner.text = `Fetching pack info for ${packageSlug}...`;
|
|
35
|
+
|
|
36
|
+
const { data: pack, error } = await supabase
|
|
37
|
+
.from('packs')
|
|
38
|
+
.select('*')
|
|
39
|
+
.eq('slug', packageSlug)
|
|
40
|
+
.eq('status', 'approved')
|
|
41
|
+
.maybeSingle();
|
|
42
|
+
|
|
43
|
+
if (error) {
|
|
44
|
+
spinner.fail('Error fetching pack from registry.');
|
|
45
|
+
console.error(chalk.gray(error.message));
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!pack) {
|
|
50
|
+
// Try local registry as fallback for dev
|
|
51
|
+
spinner.text = 'Pack not in marketplace, checking local registry...';
|
|
52
|
+
const localPack = await tryLocalRegistry(packageSlug);
|
|
53
|
+
if (localPack) {
|
|
54
|
+
await installFromLocal(localPack, packageSlug, aiDir, manifestPath, spinner);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
spinner.fail(`Package "${packageSlug}" not found in marketplace.`);
|
|
59
|
+
console.log(chalk.gray('Check the slug or browse available packs at https://contextai.io/browse'));
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
spinner.text = `Found ${pack.title} by ${pack.creator_id ? 'verified creator' : 'community'}`;
|
|
64
|
+
|
|
65
|
+
// 2. Check if paid pack
|
|
66
|
+
if (pack.price && pack.price > 0) {
|
|
67
|
+
spinner.text = 'This is a paid pack. Checking purchase status...';
|
|
68
|
+
|
|
69
|
+
const creds = loadCredentials();
|
|
70
|
+
if (!creds) {
|
|
71
|
+
spinner.fail('This pack requires purchase.');
|
|
72
|
+
console.log(chalk.yellow('\n💡 Please login first:'));
|
|
73
|
+
console.log(chalk.cyan(' contextai login\n'));
|
|
74
|
+
console.log(chalk.gray(`Then purchase at: https://contextai.io/pack/${pack.slug}`));
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// 3. Verify purchase
|
|
79
|
+
const { data: purchase } = await supabase
|
|
80
|
+
.from('purchases')
|
|
81
|
+
.select('id')
|
|
82
|
+
.eq('user_id', creds.user_id)
|
|
83
|
+
.eq('pack_id', pack.id)
|
|
84
|
+
.maybeSingle();
|
|
85
|
+
|
|
86
|
+
if (!purchase) {
|
|
87
|
+
spinner.fail('You have not purchased this pack.');
|
|
88
|
+
console.log(chalk.yellow(`\n💰 Price: ₹${pack.price}`));
|
|
89
|
+
console.log(chalk.cyan(` Purchase at: https://contextai.io/pack/${pack.slug}\n`));
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
spinner.text = 'Purchase verified! Downloading...';
|
|
94
|
+
} else {
|
|
95
|
+
// Free Pack - Enforce Limits
|
|
96
|
+
const creds = loadCredentials();
|
|
97
|
+
if (creds) {
|
|
98
|
+
const { data: profile } = await supabase
|
|
99
|
+
.from('profiles')
|
|
100
|
+
.select('download_count')
|
|
101
|
+
.eq('id', creds.user_id)
|
|
102
|
+
.single();
|
|
103
|
+
|
|
104
|
+
// TODO: Un-comment when profiles table is backfilled
|
|
105
|
+
// if (profile && profile.download_count >= 5) {
|
|
106
|
+
// spinner.fail('Free tier limit reached (5 downloads/mo).');
|
|
107
|
+
// console.log(chalk.yellow('\n⚡ Upgrade to Pro for unlimited downloads:'));
|
|
108
|
+
// console.log(chalk.cyan(' https://contextai.io/pricing\n'));
|
|
109
|
+
// process.exit(1);
|
|
110
|
+
// }
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Track Download (user-level)
|
|
115
|
+
const creds = loadCredentials();
|
|
116
|
+
if (creds) {
|
|
117
|
+
await supabase.rpc('increment_download_count', { user_id: creds.user_id });
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Track Download (pack-level)
|
|
121
|
+
await supabase.rpc('increment_download', { pack_slug: packageSlug });
|
|
122
|
+
|
|
123
|
+
// 4. Resolve target directory
|
|
124
|
+
let targetDir = '';
|
|
125
|
+
if (packageSlug.startsWith('@')) {
|
|
126
|
+
const [scope, name] = packageSlug.substring(1).split('/');
|
|
127
|
+
targetDir = path.join(aiDir, '@' + scope, name);
|
|
128
|
+
} else {
|
|
129
|
+
targetDir = path.join(aiDir, packageSlug);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// 5. Download and extract
|
|
133
|
+
if (!pack.file_url) {
|
|
134
|
+
spinner.fail('Pack has no downloadable file.');
|
|
135
|
+
process.exit(1);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
spinner.text = `Downloading from storage...`;
|
|
139
|
+
|
|
140
|
+
const response = await fetch(pack.file_url);
|
|
141
|
+
if (!response.ok) {
|
|
142
|
+
throw new Error(`Download failed: ${response.statusText}`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
146
|
+
const buffer = Buffer.from(arrayBuffer);
|
|
147
|
+
|
|
148
|
+
// Check if it's a zip or a single file
|
|
149
|
+
const isZip = pack.file_url.endsWith('.zip') || buffer[0] === 0x50; // PK signature
|
|
150
|
+
|
|
151
|
+
if (isZip) {
|
|
152
|
+
const zip = new AdmZip(buffer);
|
|
153
|
+
const tempDir = path.join(os.tmpdir(), `contextai-${Date.now()}`);
|
|
154
|
+
zip.extractAllTo(tempDir, true);
|
|
155
|
+
|
|
156
|
+
// Find actual root
|
|
157
|
+
const items = fs.readdirSync(tempDir);
|
|
158
|
+
const rootItem = items.find(i => fs.statSync(path.join(tempDir, i)).isDirectory());
|
|
159
|
+
const sourceDir = rootItem ? path.join(tempDir, rootItem) : tempDir;
|
|
160
|
+
|
|
161
|
+
spinner.text = 'Injecting Verified Context...';
|
|
162
|
+
copyRecursiveSync(sourceDir, targetDir);
|
|
163
|
+
|
|
164
|
+
// Cleanup
|
|
165
|
+
try { fs.rmSync(tempDir, { recursive: true, force: true }); } catch (e) { }
|
|
166
|
+
} else {
|
|
167
|
+
// Single file (like .cursorrules)
|
|
168
|
+
if (!fs.existsSync(targetDir)) {
|
|
169
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
170
|
+
}
|
|
171
|
+
const filename = path.basename(pack.file_url);
|
|
172
|
+
fs.writeFileSync(path.join(targetDir, filename), buffer);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// 6. Update manifest
|
|
176
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
177
|
+
manifest.packages = manifest.packages || {};
|
|
178
|
+
manifest.packages[packageSlug] = {
|
|
179
|
+
type: 'community',
|
|
180
|
+
path: `./${path.relative(aiDir, targetDir).replace(/\\/g, '/')}`,
|
|
181
|
+
installedAt: new Date().toISOString(),
|
|
182
|
+
version: pack.version || '1.0.0'
|
|
183
|
+
};
|
|
184
|
+
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
|
185
|
+
|
|
186
|
+
// 7. Update README
|
|
187
|
+
const readmePath = path.join(aiDir, 'README.md');
|
|
188
|
+
if (fs.existsSync(readmePath)) {
|
|
189
|
+
let readme = fs.readFileSync(readmePath, 'utf8');
|
|
190
|
+
const entryLine = `**${packageSlug}**: [View Context](${manifest.packages[packageSlug].path}/README.md)`;
|
|
191
|
+
if (!readme.includes(packageSlug)) {
|
|
192
|
+
if (readme.includes('## Active Protocols')) {
|
|
193
|
+
readme += `\n- ${entryLine}`;
|
|
194
|
+
} else {
|
|
195
|
+
readme += `\n\n## Installed Packages\n- ${entryLine}`;
|
|
196
|
+
}
|
|
197
|
+
fs.writeFileSync(readmePath, readme);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
spinner.succeed(`Successfully installed ${pack.title}!`);
|
|
202
|
+
console.log(chalk.gray(` Context Pack installed to: ${targetDir}`));
|
|
203
|
+
if (pack.price > 0) {
|
|
204
|
+
console.log(chalk.green(' ✓ Paid pack - thank you for your purchase!'));
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
} catch (err) {
|
|
208
|
+
spinner.fail('Installation failed:');
|
|
209
|
+
console.error(chalk.red(err.message));
|
|
210
|
+
process.exit(1);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Fallback for local development registry
|
|
215
|
+
async function tryLocalRegistry(packageName) {
|
|
216
|
+
let registryPath = path.resolve(process.cwd(), 'ucp-registry');
|
|
217
|
+
if (!fs.existsSync(registryPath)) {
|
|
218
|
+
registryPath = path.resolve(process.cwd(), '..', 'ucp-registry');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (fs.existsSync(path.join(registryPath, 'index.json'))) {
|
|
222
|
+
try {
|
|
223
|
+
const registry = JSON.parse(fs.readFileSync(path.join(registryPath, 'index.json'), 'utf8'));
|
|
224
|
+
const pkg = registry.packages[packageName];
|
|
225
|
+
if (pkg) {
|
|
226
|
+
return { ...pkg, registryPath };
|
|
227
|
+
}
|
|
228
|
+
} catch (e) {
|
|
229
|
+
// Ignore
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async function installFromLocal(localPkg, packageName, aiDir, manifestPath, spinner) {
|
|
236
|
+
spinner.text = `Installing ${packageName} from local registry...`;
|
|
237
|
+
|
|
238
|
+
let targetDir = '';
|
|
239
|
+
if (localPkg.type === 'core') {
|
|
240
|
+
targetDir = path.join(aiDir, 'ucp');
|
|
241
|
+
} else if (packageName.startsWith('@')) {
|
|
242
|
+
const [scope, name] = packageName.substring(1).split('/');
|
|
243
|
+
targetDir = path.join(aiDir, '@' + scope, name);
|
|
244
|
+
} else {
|
|
245
|
+
targetDir = path.join(aiDir, packageName);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const sourceDir = path.join(localPkg.registryPath, localPkg.path);
|
|
249
|
+
if (!fs.existsSync(sourceDir)) {
|
|
250
|
+
spinner.fail(`Package content missing at ${sourceDir}`);
|
|
251
|
+
process.exit(1);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
copyRecursiveSync(sourceDir, targetDir);
|
|
255
|
+
|
|
256
|
+
// Update manifest
|
|
257
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
258
|
+
manifest.packages = manifest.packages || {};
|
|
259
|
+
manifest.packages[packageName] = {
|
|
260
|
+
type: localPkg.type,
|
|
261
|
+
path: `./${path.relative(aiDir, targetDir).replace(/\\/g, '/')}`,
|
|
262
|
+
installedAt: new Date().toISOString()
|
|
263
|
+
};
|
|
264
|
+
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
|
265
|
+
|
|
266
|
+
spinner.succeed(`Successfully installed ${packageName} from local registry!`);
|
|
267
|
+
console.log(chalk.gray(` Location: ${targetDir}`));
|
|
268
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
|
|
5
|
+
export function publish() {
|
|
6
|
+
console.log(chalk.blue('📦 Validating package for publication...'));
|
|
7
|
+
|
|
8
|
+
const cwd = process.cwd();
|
|
9
|
+
// Validation 1: Must have README.md
|
|
10
|
+
if (!fs.existsSync(path.join(cwd, 'README.md'))) {
|
|
11
|
+
console.error(chalk.red('❌ Missing README.md'));
|
|
12
|
+
console.error(chalk.gray(' Please create a README.md describing your context package.'));
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Validation 2: Must have context/ directory or bin/ directory
|
|
17
|
+
// A valid package must have substantial content.
|
|
18
|
+
const hasContext = fs.existsSync(path.join(cwd, 'context'));
|
|
19
|
+
const hasBin = fs.existsSync(path.join(cwd, 'bin'));
|
|
20
|
+
|
|
21
|
+
if (!hasContext && !hasBin) {
|
|
22
|
+
console.error(chalk.red('❌ Invalid Package Structure'));
|
|
23
|
+
console.error(chalk.gray(' Must contain a "context/" or "bin/" directory.'));
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
console.log(chalk.green('✅ Package validation passed!'));
|
|
28
|
+
console.log('');
|
|
29
|
+
console.log('To publish your package:');
|
|
30
|
+
console.log('1. Push this directory to a public GitHub repository.');
|
|
31
|
+
console.log('2. Submit a Pull Request to the ContextAI Registry:');
|
|
32
|
+
console.log(chalk.blue(' https://github.com/contextai/registry'));
|
|
33
|
+
console.log('');
|
|
34
|
+
console.log('Add your package to index.json:');
|
|
35
|
+
console.log(chalk.gray(`
|
|
36
|
+
"your-package-name": {
|
|
37
|
+
"type": "feature",
|
|
38
|
+
"url": "https://github.com/your-username/your-repo/archive/refs/heads/main.zip",
|
|
39
|
+
"description": "Description of your package"
|
|
40
|
+
}
|
|
41
|
+
`));
|
|
42
|
+
}
|
package/src/utils/fs.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { smartMergeFile } from './merge.js';
|
|
4
|
+
|
|
5
|
+
export function copyRecursiveSync(src, dest) {
|
|
6
|
+
const exists = fs.existsSync(src);
|
|
7
|
+
const stats = exists && fs.statSync(src);
|
|
8
|
+
const isDirectory = exists && stats.isDirectory();
|
|
9
|
+
|
|
10
|
+
if (isDirectory) {
|
|
11
|
+
if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
|
|
12
|
+
fs.readdirSync(src).forEach(childItemName => {
|
|
13
|
+
copyRecursiveSync(path.join(src, childItemName), path.join(dest, childItemName));
|
|
14
|
+
});
|
|
15
|
+
} else {
|
|
16
|
+
const ext = path.extname(src);
|
|
17
|
+
|
|
18
|
+
// Only smart merge specific types that accept utf8 content
|
|
19
|
+
if (['.json', '.md'].includes(ext)) {
|
|
20
|
+
const content = fs.readFileSync(src, 'utf8');
|
|
21
|
+
smartMergeFile(dest, content, ext);
|
|
22
|
+
} else {
|
|
23
|
+
// Binary safe copy
|
|
24
|
+
fs.copyFileSync(src, dest);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Deep merges two objects.
|
|
6
|
+
* @param {Object} target
|
|
7
|
+
* @param {Object} source
|
|
8
|
+
* @returns {Object}
|
|
9
|
+
*/
|
|
10
|
+
function deepMerge(target, source) {
|
|
11
|
+
if (typeof target !== 'object' || target === null) return source;
|
|
12
|
+
if (typeof source !== 'object' || source === null) return target;
|
|
13
|
+
|
|
14
|
+
const output = { ...target };
|
|
15
|
+
|
|
16
|
+
Object.keys(source).forEach(key => {
|
|
17
|
+
if (typeof source[key] === 'object' && source[key] !== null && !Array.isArray(source[key])) {
|
|
18
|
+
if (!(key in target)) {
|
|
19
|
+
Object.assign(output, { [key]: source[key] });
|
|
20
|
+
} else {
|
|
21
|
+
output[key] = deepMerge(target[key], source[key]);
|
|
22
|
+
}
|
|
23
|
+
} else {
|
|
24
|
+
Object.assign(output, { [key]: source[key] });
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
return output;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Merges two Markdown strings.
|
|
33
|
+
* Appends the source content to the target content if it's not already present.
|
|
34
|
+
* Uses a heuristic based on checking for unique headers or content blocks if possible.
|
|
35
|
+
* For MVP: Naive append with check.
|
|
36
|
+
*
|
|
37
|
+
* @param {string} targetContent
|
|
38
|
+
* @param {string} sourceContent
|
|
39
|
+
* @returns {string}
|
|
40
|
+
*/
|
|
41
|
+
function mergeMarkdown(targetContent, sourceContent) {
|
|
42
|
+
// Normalize newlines
|
|
43
|
+
const normalizedTarget = targetContent.replace(/\r\n/g, '\n');
|
|
44
|
+
const normalizedSource = sourceContent.replace(/\r\n/g, '\n');
|
|
45
|
+
|
|
46
|
+
// Simple duplicate check: if the source content (trimmed) is fully inside target, skip or duplicate?
|
|
47
|
+
// Better: Check for key headers.
|
|
48
|
+
// MVP Strategy: Just append if not exactly present.
|
|
49
|
+
// Ideally we want to prevent adding the same "Dependency" twice.
|
|
50
|
+
|
|
51
|
+
if (normalizedTarget.includes(normalizedSource.trim())) {
|
|
52
|
+
return targetContent;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Add a separator if needed
|
|
56
|
+
const sep = normalizedTarget.endsWith('\n') ? '\n' : '\n\n';
|
|
57
|
+
return targetContent + sep + sourceContent;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Smart Merge File
|
|
62
|
+
* Decides how to merge based on file extension.
|
|
63
|
+
* @param {string} targetPath - Path to the existing file
|
|
64
|
+
* @param {string} sourceContent - Content to merge in (string or buffer)
|
|
65
|
+
* @param {string} fileExt - Extension (e.g., .json, .md)
|
|
66
|
+
*/
|
|
67
|
+
export function smartMergeFile(targetPath, sourceContent, fileExt) {
|
|
68
|
+
if (!fs.existsSync(targetPath)) {
|
|
69
|
+
fs.writeFileSync(targetPath, sourceContent);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const currentContent = fs.readFileSync(targetPath, 'utf8');
|
|
74
|
+
|
|
75
|
+
if (fileExt === '.json') {
|
|
76
|
+
try {
|
|
77
|
+
const targetObj = JSON.parse(currentContent);
|
|
78
|
+
const sourceObj = JSON.parse(sourceContent);
|
|
79
|
+
const merged = deepMerge(targetObj, sourceObj);
|
|
80
|
+
fs.writeFileSync(targetPath, JSON.stringify(merged, null, 2));
|
|
81
|
+
} catch (e) {
|
|
82
|
+
console.warn(`Failed to merge JSON for ${targetPath}. Overwriting fallback skipped (safe mode).`);
|
|
83
|
+
// fs.writeFileSync(targetPath, sourceContent); // Dangerous fallback
|
|
84
|
+
}
|
|
85
|
+
} else if (fileExt === '.md') {
|
|
86
|
+
const merged = mergeMarkdown(currentContent, sourceContent);
|
|
87
|
+
fs.writeFileSync(targetPath, merged);
|
|
88
|
+
} else {
|
|
89
|
+
// Text/Binary: Overwrite (Standard behavior for "install")
|
|
90
|
+
// or Append?
|
|
91
|
+
// Default Installing typically overwrites files unless they are config.
|
|
92
|
+
// For ContextAI: "Immutable Engine, Mutable Data"
|
|
93
|
+
// If it's in `bin/`, overwrite. If `context/`, maybe append?
|
|
94
|
+
// For now: Overwrite.
|
|
95
|
+
fs.writeFileSync(targetPath, sourceContent);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { createClient } from '@supabase/supabase-js';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
|
|
6
|
+
// Load .env file from CLI directory or parent
|
|
7
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const envPaths = [
|
|
9
|
+
path.join(__dirname, '..', '..', '.env'), // cli/.env
|
|
10
|
+
path.join(__dirname, '..', '..', '..', '.env'), // parent contextai/.env
|
|
11
|
+
path.join(process.cwd(), '.env') // current working directory
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
for (const envPath of envPaths) {
|
|
15
|
+
if (fs.existsSync(envPath)) {
|
|
16
|
+
const envConfig = fs.readFileSync(envPath, 'utf8');
|
|
17
|
+
envConfig.split('\n').forEach(line => {
|
|
18
|
+
const [key, ...valueParts] = line.split('=');
|
|
19
|
+
const value = valueParts.join('='); // Handle = in values
|
|
20
|
+
if (key && value && !process.env[key.trim()]) {
|
|
21
|
+
process.env[key.trim()] = value.trim();
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
break;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const SUPABASE_URL = process.env.VITE_SUPABASE_URL;
|
|
29
|
+
const SUPABASE_KEY = process.env.VITE_SUPABASE_ANON_KEY;
|
|
30
|
+
|
|
31
|
+
if (!SUPABASE_URL || !SUPABASE_KEY) {
|
|
32
|
+
console.error('Error: VITE_SUPABASE_URL and VITE_SUPABASE_ANON_KEY must be set.');
|
|
33
|
+
console.error('Create a .env file or set environment variables.');
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const supabase = createClient(SUPABASE_URL, SUPABASE_KEY);
|