decisionnode 0.2.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.
@@ -0,0 +1,300 @@
1
+ /**
2
+ * DecisionNode Marketplace
3
+ *
4
+ * Download and install curated decision packs. Embeddings are generated locally.
5
+ */
6
+ import fs from 'fs/promises';
7
+ import path from 'path';
8
+ import { getProjectRoot, ensureProjectFolder } from './env.js';
9
+ import { logAction } from './history.js';
10
+ // Supabase configuration for the official DecisionNode Marketplace
11
+ // Users can override with environment variables from ~/.decisionnode/.env
12
+ const SUPABASE_URL = process.env.SUPABASE_URL || '';
13
+ const SUPABASE_ANON_KEY = process.env.SUPABASE_ANON_KEY || '';
14
+ /**
15
+ * Check if Supabase is configured
16
+ */
17
+ function isSupabaseConfigured() {
18
+ return Boolean(SUPABASE_URL && SUPABASE_ANON_KEY);
19
+ }
20
+ /**
21
+ * Get helpful error message for missing Supabase config
22
+ */
23
+ function getSupabaseConfigError() {
24
+ const homeDir = process.env.HOME || process.env.USERPROFILE || '~';
25
+ return `Marketplace requires Supabase configuration.\n` +
26
+ `Please add the following to ${homeDir}/.decisionnode/.env:\n\n` +
27
+ ` SUPABASE_URL=https://your-project.supabase.co\n` +
28
+ ` SUPABASE_ANON_KEY=your-anon-key\n\n` +
29
+ `You can find these values in your Supabase project dashboard.`;
30
+ }
31
+ // Legacy GitHub fallback (for sample packs)
32
+ const MARKETPLACE_BASE = 'https://raw.githubusercontent.com/decisionnode/marketplace/main';
33
+ /**
34
+ * Fetch the marketplace index from Supabase
35
+ */
36
+ export async function getMarketplaceIndex() {
37
+ // Check if Supabase is configured
38
+ if (!isSupabaseConfigured()) {
39
+ console.error(getSupabaseConfigError());
40
+ return getSamplePacks();
41
+ }
42
+ try {
43
+ // Fetch from Supabase REST API
44
+ const response = await fetch(`${SUPABASE_URL}/rest/v1/packs_with_ratings?select=slug,name,description,author_username,scope,decisions,tags,downloads`, {
45
+ headers: {
46
+ 'apikey': SUPABASE_ANON_KEY,
47
+ 'Authorization': `Bearer ${SUPABASE_ANON_KEY}`,
48
+ 'Content-Type': 'application/json'
49
+ }
50
+ });
51
+ if (!response.ok) {
52
+ throw new Error(`Failed to fetch marketplace index: ${response.statusText}`);
53
+ }
54
+ const packs = await response.json();
55
+ // Transform to MarketplaceEntry format
56
+ return packs.map(pack => ({
57
+ id: pack.slug,
58
+ name: pack.name,
59
+ description: pack.description,
60
+ author: pack.author_username || 'Unknown',
61
+ scope: pack.scope,
62
+ decisionCount: Array.isArray(pack.decisions) ? pack.decisions.length : 0,
63
+ downloads: pack.downloads || 0
64
+ }));
65
+ }
66
+ catch (error) {
67
+ console.error('Failed to fetch from Supabase, using sample packs:', error);
68
+ // Return sample packs as fallback
69
+ return getSamplePacks();
70
+ }
71
+ }
72
+ /**
73
+ * Get sample packs for demo/development
74
+ */
75
+ function getSamplePacks() {
76
+ return [
77
+ {
78
+ id: 'ui-modern-web',
79
+ name: 'Modern Web UI',
80
+ description: 'Best practices for modern web interfaces: colors, typography, spacing, animations',
81
+ author: 'DecisionNode',
82
+ scope: 'UI',
83
+ decisionCount: 12,
84
+ downloads: 1250
85
+ },
86
+ {
87
+ id: 'api-rest-best-practices',
88
+ name: 'REST API Best Practices',
89
+ description: 'RESTful API design patterns, versioning, error handling, authentication',
90
+ author: 'DecisionNode',
91
+ scope: 'API',
92
+ decisionCount: 15,
93
+ downloads: 890
94
+ },
95
+ {
96
+ id: 'backend-nodejs',
97
+ name: 'Node.js Backend',
98
+ description: 'Node.js architecture decisions: error handling, logging, security, testing',
99
+ author: 'DecisionNode',
100
+ scope: 'Backend',
101
+ decisionCount: 18,
102
+ downloads: 670
103
+ },
104
+ {
105
+ id: 'react-architecture',
106
+ name: 'React Architecture',
107
+ description: 'React best practices: component patterns, state management, hooks, testing',
108
+ author: 'DecisionNode',
109
+ scope: 'Frontend',
110
+ decisionCount: 20,
111
+ downloads: 1560
112
+ }
113
+ ];
114
+ }
115
+ /**
116
+ * Download and install a decision pack
117
+ */
118
+ export async function installPack(packId) {
119
+ // For demo, generate sample pack
120
+ const pack = await fetchPack(packId);
121
+ if (!pack) {
122
+ throw new Error(`Pack ${packId} not found`);
123
+ }
124
+ ensureProjectFolder();
125
+ const projectRoot = getProjectRoot();
126
+ // Load existing decisions for this scope
127
+ const scopeFile = path.join(projectRoot, `${pack.scope.toLowerCase()}.json`);
128
+ let existing = { scope: pack.scope.toLowerCase(), decisions: [] };
129
+ try {
130
+ const content = await fs.readFile(scopeFile, 'utf-8');
131
+ existing = JSON.parse(content);
132
+ }
133
+ catch {
134
+ // File doesn't exist yet
135
+ }
136
+ // Track existing IDs to avoid duplicates
137
+ const existingIds = new Set(existing.decisions.map(d => d.id));
138
+ let installed = 0;
139
+ let skipped = 0;
140
+ // Add new decisions (with new IDs to avoid conflicts)
141
+ for (const decision of pack.decisions) {
142
+ // Generate new ID based on scope and count
143
+ const newId = `${pack.scope.toLowerCase()}-${(existing.decisions.length + installed + 1).toString().padStart(3, '0')}`;
144
+ // Check if a very similar decision already exists
145
+ const isDuplicate = existing.decisions.some(d => d.decision.toLowerCase() === decision.decision.toLowerCase());
146
+ if (isDuplicate) {
147
+ skipped++;
148
+ continue;
149
+ }
150
+ existing.decisions.push({
151
+ ...decision,
152
+ id: newId,
153
+ createdAt: new Date().toISOString()
154
+ });
155
+ installed++;
156
+ }
157
+ // Save updated decisions
158
+ await fs.writeFile(scopeFile, JSON.stringify(existing, null, 2), 'utf-8');
159
+ // Merge vectors from pack
160
+ const vectorsFile = path.join(projectRoot, 'vectors.json');
161
+ let existingVectors = {};
162
+ try {
163
+ const content = await fs.readFile(vectorsFile, 'utf-8');
164
+ existingVectors = JSON.parse(content);
165
+ }
166
+ catch {
167
+ // File doesn't exist yet
168
+ }
169
+ // Add pack vectors with new IDs
170
+ let vectorIndex = 0;
171
+ for (const decision of pack.decisions) {
172
+ const originalId = decision.id;
173
+ const newId = `${pack.scope.toLowerCase()}-${(existing.decisions.length - pack.decisions.length + vectorIndex + 1).toString().padStart(3, '0')}`;
174
+ if (pack.vectors[originalId]) {
175
+ existingVectors[newId] = pack.vectors[originalId];
176
+ }
177
+ vectorIndex++;
178
+ }
179
+ await fs.writeFile(vectorsFile, JSON.stringify(existingVectors, null, 2), 'utf-8');
180
+ // Log the installation to history
181
+ if (installed > 0) {
182
+ await logAction('installed', `${pack.id}`, `Installed pack "${pack.name}" (${installed} decisions)`);
183
+ }
184
+ return { installed, skipped };
185
+ }
186
+ /**
187
+ * Fetch a specific pack from Supabase by slug
188
+ */
189
+ async function fetchPack(packId) {
190
+ // Check if Supabase is configured
191
+ if (!isSupabaseConfigured()) {
192
+ console.error(getSupabaseConfigError());
193
+ return generateSamplePack(packId);
194
+ }
195
+ try {
196
+ // Fetch from Supabase REST API
197
+ const response = await fetch(`${SUPABASE_URL}/rest/v1/packs_with_ratings?slug=eq.${encodeURIComponent(packId)}&select=*`, {
198
+ headers: {
199
+ 'apikey': SUPABASE_ANON_KEY,
200
+ 'Authorization': `Bearer ${SUPABASE_ANON_KEY}`,
201
+ 'Content-Type': 'application/json'
202
+ }
203
+ });
204
+ if (!response.ok) {
205
+ throw new Error(`Failed to fetch pack: ${response.statusText}`);
206
+ }
207
+ const packs = await response.json();
208
+ if (packs.length === 0) {
209
+ // Try legacy sample pack
210
+ return generateSamplePack(packId);
211
+ }
212
+ const pack = packs[0];
213
+ // Transform to DecisionPack format
214
+ return {
215
+ id: pack.slug,
216
+ name: pack.name,
217
+ description: pack.description,
218
+ author: pack.author_username || 'Unknown',
219
+ version: pack.version || '1.0.0',
220
+ scope: pack.scope,
221
+ decisions: pack.decisions || [],
222
+ vectors: pack.vectors || {}
223
+ };
224
+ }
225
+ catch (error) {
226
+ console.error('Failed to fetch pack from Supabase:', error);
227
+ // Fall through to sample pack
228
+ return generateSamplePack(packId);
229
+ }
230
+ }
231
+ /**
232
+ * Generate a sample pack for demonstration
233
+ */
234
+ function generateSamplePack(packId) {
235
+ const samplePacks = {
236
+ 'ui-modern-web': {
237
+ id: 'ui-modern-web',
238
+ name: 'Modern Web UI',
239
+ description: 'Best practices for modern web interfaces',
240
+ author: 'DecisionNode',
241
+ version: '1.0.0',
242
+ scope: 'UI',
243
+ decisions: [
244
+ {
245
+ id: 'pack-001',
246
+ scope: 'ui',
247
+ decision: 'Use a consistent 8px spacing grid for all layout decisions',
248
+ rationale: '8px grid provides visual rhythm and makes responsive design easier',
249
+ constraints: ['Margins and padding must be multiples of 8px', 'Icon sizes should be 16, 24, 32, or 48px'],
250
+ status: 'active',
251
+ createdAt: new Date().toISOString()
252
+ },
253
+ {
254
+ id: 'pack-002',
255
+ scope: 'ui',
256
+ decision: 'Use Inter or system fonts for body text, display fonts for headings only',
257
+ rationale: 'Inter is highly readable and open source, system fonts ensure fast loading',
258
+ constraints: ['Minimum body font size is 16px', 'Line height should be 1.5 for body text'],
259
+ status: 'active',
260
+ createdAt: new Date().toISOString()
261
+ },
262
+ {
263
+ id: 'pack-003',
264
+ scope: 'ui',
265
+ decision: 'All interactive elements must have visible focus states',
266
+ rationale: 'Accessibility requirement for keyboard users',
267
+ constraints: ['Focus ring must have 2px offset', 'Focus color must have 3:1 contrast ratio'],
268
+ status: 'active',
269
+ createdAt: new Date().toISOString()
270
+ },
271
+ {
272
+ id: 'pack-004',
273
+ scope: 'ui',
274
+ decision: 'Animations should complete within 300ms for UI feedback',
275
+ rationale: 'Longer animations feel sluggish, shorter ones are jarring',
276
+ constraints: ['Use ease-out for entrances', 'Use ease-in for exits', 'Disable for reduced-motion preference'],
277
+ status: 'active',
278
+ createdAt: new Date().toISOString()
279
+ }
280
+ ],
281
+ vectors: {
282
+ 'pack-001': new Array(768).fill(0).map(() => Math.random() * 2 - 1),
283
+ 'pack-002': new Array(768).fill(0).map(() => Math.random() * 2 - 1),
284
+ 'pack-003': new Array(768).fill(0).map(() => Math.random() * 2 - 1),
285
+ 'pack-004': new Array(768).fill(0).map(() => Math.random() * 2 - 1)
286
+ }
287
+ }
288
+ };
289
+ return samplePacks[packId] || null;
290
+ }
291
+ /**
292
+ * Search marketplace by query
293
+ */
294
+ export async function searchMarketplace(query) {
295
+ const index = await getMarketplaceIndex();
296
+ const lowerQuery = query.toLowerCase();
297
+ return index.filter(entry => entry.name.toLowerCase().includes(lowerQuery) ||
298
+ entry.description.toLowerCase().includes(lowerQuery) ||
299
+ entry.scope.toLowerCase().includes(lowerQuery));
300
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};