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.
- package/LICENSE +21 -0
- package/README.md +80 -0
- package/dist/ai/gemini.d.ts +15 -0
- package/dist/ai/gemini.js +56 -0
- package/dist/ai/rag.d.ts +79 -0
- package/dist/ai/rag.js +268 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +1724 -0
- package/dist/cloud.d.ts +177 -0
- package/dist/cloud.js +631 -0
- package/dist/env.d.ts +47 -0
- package/dist/env.js +139 -0
- package/dist/history.d.ts +34 -0
- package/dist/history.js +159 -0
- package/dist/maintenance.d.ts +7 -0
- package/dist/maintenance.js +49 -0
- package/dist/marketplace.d.ts +46 -0
- package/dist/marketplace.js +300 -0
- package/dist/mcp/server.d.ts +2 -0
- package/dist/mcp/server.js +621 -0
- package/dist/setup.d.ts +6 -0
- package/dist/setup.js +132 -0
- package/dist/store.d.ts +126 -0
- package/dist/store.js +555 -0
- package/dist/types.d.ts +60 -0
- package/dist/types.js +9 -0
- package/package.json +57 -0
|
@@ -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
|
+
}
|