@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 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
+ }
@@ -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);