@girardmedia/bootspring 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +255 -0
- package/agents/README.md +93 -0
- package/agents/api-expert/context.md +416 -0
- package/agents/architecture-expert/context.md +454 -0
- package/agents/backend-expert/context.md +483 -0
- package/agents/code-review-expert/context.md +365 -0
- package/agents/database-expert/context.md +250 -0
- package/agents/devops-expert/context.md +446 -0
- package/agents/frontend-expert/context.md +364 -0
- package/agents/index.js +140 -0
- package/agents/performance-expert/context.md +377 -0
- package/agents/security-expert/context.md +343 -0
- package/agents/testing-expert/context.md +414 -0
- package/agents/ui-ux-expert/context.md +448 -0
- package/agents/vercel-expert/context.md +426 -0
- package/bin/bootspring.js +310 -0
- package/cli/agent.js +337 -0
- package/cli/context.js +194 -0
- package/cli/dashboard.js +150 -0
- package/cli/generate.js +294 -0
- package/cli/init.js +410 -0
- package/cli/loop.js +421 -0
- package/cli/mcp.js +241 -0
- package/cli/memory.js +303 -0
- package/cli/orchestrator.js +400 -0
- package/cli/plugin.js +451 -0
- package/cli/quality.js +332 -0
- package/cli/skill.js +369 -0
- package/cli/task.js +628 -0
- package/cli/telemetry.js +114 -0
- package/cli/todo.js +614 -0
- package/cli/update.js +312 -0
- package/core/config.js +245 -0
- package/core/context.js +329 -0
- package/core/entitlements.js +209 -0
- package/core/index.js +43 -0
- package/core/policies.js +68 -0
- package/core/telemetry.js +247 -0
- package/core/utils.js +380 -0
- package/dashboard/server.js +818 -0
- package/docs/integrations/claude-code.md +42 -0
- package/docs/integrations/codex.md +42 -0
- package/docs/mcp-api-platform.md +102 -0
- package/generators/generate.js +598 -0
- package/generators/index.js +18 -0
- package/hooks/context-detector.js +177 -0
- package/hooks/index.js +35 -0
- package/hooks/prompt-enhancer.js +289 -0
- package/intelligence/git-memory.js +551 -0
- package/intelligence/index.js +59 -0
- package/intelligence/orchestrator.js +964 -0
- package/intelligence/prd.js +447 -0
- package/intelligence/recommendation-weights.json +18 -0
- package/intelligence/recommendations.js +234 -0
- package/mcp/capabilities.js +71 -0
- package/mcp/contracts/mcp-contract.v1.json +497 -0
- package/mcp/registry.js +213 -0
- package/mcp/response-formatter.js +462 -0
- package/mcp/server.js +99 -0
- package/mcp/tools/agent-tool.js +137 -0
- package/mcp/tools/capabilities-tool.js +54 -0
- package/mcp/tools/context-tool.js +49 -0
- package/mcp/tools/dashboard-tool.js +58 -0
- package/mcp/tools/generate-tool.js +46 -0
- package/mcp/tools/loop-tool.js +134 -0
- package/mcp/tools/memory-tool.js +180 -0
- package/mcp/tools/orchestrator-tool.js +232 -0
- package/mcp/tools/plugin-tool.js +76 -0
- package/mcp/tools/quality-tool.js +47 -0
- package/mcp/tools/skill-tool.js +233 -0
- package/mcp/tools/telemetry-tool.js +95 -0
- package/mcp/tools/todo-tool.js +133 -0
- package/package.json +98 -0
- package/plugins/index.js +141 -0
- package/quality/index.js +380 -0
- package/quality/lint-budgets.json +19 -0
- package/skills/index.js +787 -0
- package/skills/patterns/README.md +163 -0
- package/skills/patterns/api/route-handler.md +217 -0
- package/skills/patterns/api/server-action.md +249 -0
- package/skills/patterns/auth/clerk.md +132 -0
- package/skills/patterns/database/prisma.md +180 -0
- package/skills/patterns/payments/stripe.md +272 -0
- package/skills/patterns/security/validation.md +268 -0
- package/skills/patterns/testing/vitest.md +307 -0
- package/templates/bootspring.config.js +83 -0
- package/templates/mcp.json +9 -0
package/core/context.js
ADDED
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bootspring Context Manager
|
|
3
|
+
* Handles project context for AI assistants
|
|
4
|
+
*
|
|
5
|
+
* @package bootspring
|
|
6
|
+
* @module core/context
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const config = require('./config');
|
|
12
|
+
const utils = require('./utils');
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Get current project context
|
|
16
|
+
* @param {object} [options] - Options
|
|
17
|
+
* @returns {object} Context object
|
|
18
|
+
*/
|
|
19
|
+
function get(options = {}) {
|
|
20
|
+
const cfg = options.config || config.load();
|
|
21
|
+
const projectRoot = cfg._projectRoot;
|
|
22
|
+
|
|
23
|
+
const context = {
|
|
24
|
+
project: cfg.project,
|
|
25
|
+
stack: cfg.stack,
|
|
26
|
+
plugins: getEnabledPlugins(cfg),
|
|
27
|
+
files: getProjectFiles(projectRoot),
|
|
28
|
+
git: getGitInfo(projectRoot),
|
|
29
|
+
state: getProjectState(projectRoot, cfg),
|
|
30
|
+
timestamp: new Date().toISOString()
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
return context;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Get enabled plugins from config
|
|
38
|
+
* @param {object} cfg - Configuration
|
|
39
|
+
* @returns {object} Enabled plugins
|
|
40
|
+
*/
|
|
41
|
+
function getEnabledPlugins(cfg) {
|
|
42
|
+
const enabled = {};
|
|
43
|
+
|
|
44
|
+
for (const [name, plugin] of Object.entries(cfg.plugins || {})) {
|
|
45
|
+
if (plugin.enabled !== false) {
|
|
46
|
+
enabled[name] = {
|
|
47
|
+
provider: plugin.provider || 'default',
|
|
48
|
+
features: plugin.features || []
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return enabled;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Get project file structure summary
|
|
58
|
+
* @param {string} projectRoot - Project root
|
|
59
|
+
* @returns {object} File info
|
|
60
|
+
*/
|
|
61
|
+
function getProjectFiles(projectRoot) {
|
|
62
|
+
const files = {
|
|
63
|
+
hasPackageJson: utils.fileExists(path.join(projectRoot, 'package.json')),
|
|
64
|
+
hasTsConfig: utils.fileExists(path.join(projectRoot, 'tsconfig.json')),
|
|
65
|
+
hasClaudeMd: utils.fileExists(path.join(projectRoot, 'CLAUDE.md')),
|
|
66
|
+
hasBootspringConfig: utils.fileExists(path.join(projectRoot, 'bootspring.config.js')),
|
|
67
|
+
hasTodoMd: utils.fileExists(path.join(projectRoot, 'todo.md')),
|
|
68
|
+
hasGit: utils.fileExists(path.join(projectRoot, '.git')),
|
|
69
|
+
hasSrcDir: utils.fileExists(path.join(projectRoot, 'src')),
|
|
70
|
+
hasAppDir: utils.fileExists(path.join(projectRoot, 'app')),
|
|
71
|
+
hasPagesDir: utils.fileExists(path.join(projectRoot, 'pages'))
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// Detect framework structure
|
|
75
|
+
if (files.hasAppDir) {
|
|
76
|
+
files.structure = 'app-router';
|
|
77
|
+
} else if (files.hasPagesDir) {
|
|
78
|
+
files.structure = 'pages-router';
|
|
79
|
+
} else if (files.hasSrcDir) {
|
|
80
|
+
files.structure = 'src-based';
|
|
81
|
+
} else {
|
|
82
|
+
files.structure = 'flat';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return files;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Get git information
|
|
90
|
+
* @param {string} projectRoot - Project root
|
|
91
|
+
* @returns {object} Git info
|
|
92
|
+
*/
|
|
93
|
+
function getGitInfo(projectRoot) {
|
|
94
|
+
const gitDir = path.join(projectRoot, '.git');
|
|
95
|
+
|
|
96
|
+
if (!utils.fileExists(gitDir)) {
|
|
97
|
+
return { initialized: false };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const info = { initialized: true };
|
|
101
|
+
|
|
102
|
+
// Get current branch
|
|
103
|
+
const headPath = path.join(gitDir, 'HEAD');
|
|
104
|
+
if (utils.fileExists(headPath)) {
|
|
105
|
+
const head = utils.readFile(headPath).trim();
|
|
106
|
+
if (head.startsWith('ref: refs/heads/')) {
|
|
107
|
+
info.branch = head.replace('ref: refs/heads/', '');
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Check for remote
|
|
112
|
+
const configPath = path.join(gitDir, 'config');
|
|
113
|
+
if (utils.fileExists(configPath)) {
|
|
114
|
+
const gitConfig = utils.readFile(configPath);
|
|
115
|
+
info.hasRemote = gitConfig.includes('[remote "origin"]');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return info;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Get project state
|
|
123
|
+
* @param {string} projectRoot - Project root
|
|
124
|
+
* @param {object} cfg - Configuration
|
|
125
|
+
* @returns {object} State info
|
|
126
|
+
*/
|
|
127
|
+
function getProjectState(projectRoot, cfg) {
|
|
128
|
+
const state = {
|
|
129
|
+
phase: 'unknown',
|
|
130
|
+
health: 'unknown',
|
|
131
|
+
todos: 0,
|
|
132
|
+
lastGenerated: null
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
// Count todos
|
|
136
|
+
const todoPath = path.join(projectRoot, cfg.paths?.todo || 'todo.md');
|
|
137
|
+
if (utils.fileExists(todoPath)) {
|
|
138
|
+
const content = utils.readFile(todoPath);
|
|
139
|
+
const todoMatches = content.match(/- \[ \]/g);
|
|
140
|
+
state.todos = todoMatches ? todoMatches.length : 0;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Check CLAUDE.md generation time
|
|
144
|
+
const claudePath = path.join(projectRoot, cfg.paths?.context || 'CLAUDE.md');
|
|
145
|
+
if (utils.fileExists(claudePath)) {
|
|
146
|
+
state.lastGenerated = utils.getFileTime(claudePath);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Determine phase
|
|
150
|
+
if (!cfg._configPath) {
|
|
151
|
+
state.phase = 'uninitialized';
|
|
152
|
+
} else if (!state.lastGenerated) {
|
|
153
|
+
state.phase = 'initialized';
|
|
154
|
+
} else {
|
|
155
|
+
state.phase = 'active';
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Determine health
|
|
159
|
+
const issues = [];
|
|
160
|
+
if (!utils.fileExists(path.join(projectRoot, 'package.json'))) {
|
|
161
|
+
issues.push('missing-package-json');
|
|
162
|
+
}
|
|
163
|
+
if (!cfg._configPath) {
|
|
164
|
+
issues.push('missing-config');
|
|
165
|
+
}
|
|
166
|
+
if (!state.lastGenerated) {
|
|
167
|
+
issues.push('missing-context');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (issues.length === 0) {
|
|
171
|
+
state.health = 'good';
|
|
172
|
+
} else if (issues.length <= 2) {
|
|
173
|
+
state.health = 'fair';
|
|
174
|
+
} else {
|
|
175
|
+
state.health = 'needs-attention';
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
state.issues = issues;
|
|
179
|
+
|
|
180
|
+
return state;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Validate project context
|
|
185
|
+
* @param {object} [options] - Options
|
|
186
|
+
* @returns {object} Validation result
|
|
187
|
+
*/
|
|
188
|
+
function validate(options = {}) {
|
|
189
|
+
const cfg = options.config || config.load();
|
|
190
|
+
const projectRoot = cfg._projectRoot;
|
|
191
|
+
|
|
192
|
+
const checks = [];
|
|
193
|
+
let score = 0;
|
|
194
|
+
const maxScore = 10;
|
|
195
|
+
|
|
196
|
+
// Check 1: Config exists
|
|
197
|
+
if (cfg._configPath) {
|
|
198
|
+
checks.push({ name: 'Configuration', status: 'pass', message: 'bootspring.config.js found' });
|
|
199
|
+
score += 2;
|
|
200
|
+
} else {
|
|
201
|
+
checks.push({ name: 'Configuration', status: 'fail', message: 'bootspring.config.js missing' });
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Check 2: CLAUDE.md exists
|
|
205
|
+
const claudePath = path.join(projectRoot, cfg.paths?.context || 'CLAUDE.md');
|
|
206
|
+
if (utils.fileExists(claudePath)) {
|
|
207
|
+
checks.push({ name: 'AI Context', status: 'pass', message: 'CLAUDE.md exists' });
|
|
208
|
+
score += 2;
|
|
209
|
+
} else {
|
|
210
|
+
checks.push({ name: 'AI Context', status: 'fail', message: 'CLAUDE.md missing - run bootspring generate' });
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Check 3: package.json exists
|
|
214
|
+
if (utils.fileExists(path.join(projectRoot, 'package.json'))) {
|
|
215
|
+
checks.push({ name: 'Package', status: 'pass', message: 'package.json found' });
|
|
216
|
+
score += 1;
|
|
217
|
+
} else {
|
|
218
|
+
checks.push({ name: 'Package', status: 'warn', message: 'package.json missing' });
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Check 4: Git initialized
|
|
222
|
+
if (utils.fileExists(path.join(projectRoot, '.git'))) {
|
|
223
|
+
checks.push({ name: 'Git', status: 'pass', message: 'Git repository initialized' });
|
|
224
|
+
score += 1;
|
|
225
|
+
} else {
|
|
226
|
+
checks.push({ name: 'Git', status: 'warn', message: 'Git not initialized' });
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Check 5: TypeScript config (if using TS)
|
|
230
|
+
if (cfg.stack?.language === 'typescript') {
|
|
231
|
+
if (utils.fileExists(path.join(projectRoot, 'tsconfig.json'))) {
|
|
232
|
+
checks.push({ name: 'TypeScript', status: 'pass', message: 'tsconfig.json found' });
|
|
233
|
+
score += 1;
|
|
234
|
+
} else {
|
|
235
|
+
checks.push({ name: 'TypeScript', status: 'fail', message: 'tsconfig.json missing for TypeScript project' });
|
|
236
|
+
}
|
|
237
|
+
} else {
|
|
238
|
+
score += 1;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Check 6: Config validation
|
|
242
|
+
const configValidation = config.validate(cfg);
|
|
243
|
+
if (configValidation.valid) {
|
|
244
|
+
checks.push({ name: 'Config Validation', status: 'pass', message: 'Configuration is valid' });
|
|
245
|
+
score += 2;
|
|
246
|
+
} else {
|
|
247
|
+
checks.push({ name: 'Config Validation', status: 'fail', message: configValidation.errors.join(', ') });
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Check 7: Todo file
|
|
251
|
+
if (utils.fileExists(path.join(projectRoot, cfg.paths?.todo || 'todo.md'))) {
|
|
252
|
+
checks.push({ name: 'Todo Tracking', status: 'pass', message: 'todo.md exists' });
|
|
253
|
+
score += 1;
|
|
254
|
+
} else {
|
|
255
|
+
checks.push({ name: 'Todo Tracking', status: 'info', message: 'todo.md not found - optional' });
|
|
256
|
+
score += 0.5;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return {
|
|
260
|
+
valid: score >= maxScore * 0.6,
|
|
261
|
+
score,
|
|
262
|
+
maxScore,
|
|
263
|
+
percentage: Math.round((score / maxScore) * 100),
|
|
264
|
+
checks
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Generate context summary for AI
|
|
270
|
+
* @param {object} [options] - Options
|
|
271
|
+
* @returns {string} Context summary markdown
|
|
272
|
+
*/
|
|
273
|
+
function generateSummary(options = {}) {
|
|
274
|
+
const ctx = get(options);
|
|
275
|
+
|
|
276
|
+
const lines = [
|
|
277
|
+
`# Project Context`,
|
|
278
|
+
``,
|
|
279
|
+
`**Project**: ${ctx.project.name}`,
|
|
280
|
+
`**Generated**: ${ctx.timestamp}`,
|
|
281
|
+
``,
|
|
282
|
+
`## Stack`,
|
|
283
|
+
`- Framework: ${ctx.stack.framework}`,
|
|
284
|
+
`- Language: ${ctx.stack.language}`,
|
|
285
|
+
`- Database: ${ctx.stack.database}`,
|
|
286
|
+
`- Hosting: ${ctx.stack.hosting}`,
|
|
287
|
+
``
|
|
288
|
+
];
|
|
289
|
+
|
|
290
|
+
// Plugins
|
|
291
|
+
const enabledPlugins = Object.keys(ctx.plugins);
|
|
292
|
+
if (enabledPlugins.length > 0) {
|
|
293
|
+
lines.push(`## Enabled Plugins`);
|
|
294
|
+
for (const [name, plugin] of Object.entries(ctx.plugins)) {
|
|
295
|
+
lines.push(`- **${name}**: ${plugin.provider}`);
|
|
296
|
+
}
|
|
297
|
+
lines.push(``);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// State
|
|
301
|
+
lines.push(`## Project State`);
|
|
302
|
+
lines.push(`- Phase: ${ctx.state.phase}`);
|
|
303
|
+
lines.push(`- Health: ${ctx.state.health}`);
|
|
304
|
+
lines.push(`- Open Todos: ${ctx.state.todos}`);
|
|
305
|
+
if (ctx.state.lastGenerated) {
|
|
306
|
+
lines.push(`- Context Last Generated: ${utils.formatRelativeTime(ctx.state.lastGenerated)}`);
|
|
307
|
+
}
|
|
308
|
+
lines.push(``);
|
|
309
|
+
|
|
310
|
+
// Git
|
|
311
|
+
if (ctx.git.initialized) {
|
|
312
|
+
lines.push(`## Git`);
|
|
313
|
+
lines.push(`- Branch: ${ctx.git.branch || 'unknown'}`);
|
|
314
|
+
lines.push(`- Remote: ${ctx.git.hasRemote ? 'configured' : 'not configured'}`);
|
|
315
|
+
lines.push(``);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return lines.join('\n');
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
module.exports = {
|
|
322
|
+
get,
|
|
323
|
+
validate,
|
|
324
|
+
generateSummary,
|
|
325
|
+
getEnabledPlugins,
|
|
326
|
+
getProjectFiles,
|
|
327
|
+
getGitInfo,
|
|
328
|
+
getProjectState
|
|
329
|
+
};
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bootspring Entitlements
|
|
3
|
+
* Shared access policy for gated capabilities.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const LOCAL_MODE = 'local';
|
|
7
|
+
const SERVER_MODE = 'server';
|
|
8
|
+
const PRO_TIERS = new Set(['pro', 'team', 'enterprise']);
|
|
9
|
+
const policies = require('./policies');
|
|
10
|
+
|
|
11
|
+
function parseBoolean(value) {
|
|
12
|
+
if (typeof value === 'boolean') return value;
|
|
13
|
+
if (value === null || value === undefined) return false;
|
|
14
|
+
const normalized = String(value).trim().toLowerCase();
|
|
15
|
+
return normalized === '1' || normalized === 'true' || normalized === 'yes' || normalized === 'on';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function normalizeMode(value) {
|
|
19
|
+
const mode = String(value || '').trim().toLowerCase();
|
|
20
|
+
if (mode === SERVER_MODE) {
|
|
21
|
+
return SERVER_MODE;
|
|
22
|
+
}
|
|
23
|
+
return LOCAL_MODE;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function resolveAccessContext(options = {}) {
|
|
27
|
+
const envMode = process.env.BOOTSPRING_SKILL_ACCESS_MODE;
|
|
28
|
+
const envTier = process.env.BOOTSPRING_USER_TIER;
|
|
29
|
+
const envEntitled = process.env.BOOTSPRING_SKILLS_ENTITLED;
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
mode: normalizeMode(options.mode || envMode),
|
|
33
|
+
tier: String(options.tier || envTier || 'free').trim().toLowerCase(),
|
|
34
|
+
entitled: parseBoolean(options.entitled ?? envEntitled),
|
|
35
|
+
policyProfile: policies.resolvePolicyProfile(options)
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function resolveWorkflowAccessContext(options = {}) {
|
|
40
|
+
const envMode = process.env.BOOTSPRING_WORKFLOW_ACCESS_MODE || process.env.BOOTSPRING_SKILL_ACCESS_MODE;
|
|
41
|
+
const envTier = process.env.BOOTSPRING_USER_TIER;
|
|
42
|
+
const envEntitled = process.env.BOOTSPRING_WORKFLOWS_ENTITLED ?? process.env.BOOTSPRING_SKILLS_ENTITLED;
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
mode: normalizeMode(options.mode || envMode),
|
|
46
|
+
tier: String(options.tier || envTier || 'free').trim().toLowerCase(),
|
|
47
|
+
entitled: parseBoolean(options.entitled ?? envEntitled),
|
|
48
|
+
policyProfile: policies.resolvePolicyProfile(options)
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function isExternalSkill(skillId) {
|
|
53
|
+
return String(skillId || '').startsWith('external/');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function checkSkillAccess(skillId, options = {}) {
|
|
57
|
+
const context = resolveAccessContext(options);
|
|
58
|
+
|
|
59
|
+
if (!isExternalSkill(skillId)) {
|
|
60
|
+
return {
|
|
61
|
+
allowed: true,
|
|
62
|
+
code: 'built_in',
|
|
63
|
+
reason: 'Built-in skills are always available.',
|
|
64
|
+
context
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const policy = policies.getPolicyProfile(context.policyProfile, options);
|
|
69
|
+
if (!policy.allowExternalSkills) {
|
|
70
|
+
return {
|
|
71
|
+
allowed: false,
|
|
72
|
+
code: 'external_policy_blocked',
|
|
73
|
+
reason: `External skills are blocked by ${policy.id} policy profile.`,
|
|
74
|
+
context
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (context.mode !== SERVER_MODE) {
|
|
79
|
+
return {
|
|
80
|
+
allowed: true,
|
|
81
|
+
code: 'external_local_mode',
|
|
82
|
+
reason: 'External skills are enabled in local mode.',
|
|
83
|
+
context
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (context.entitled || PRO_TIERS.has(context.tier)) {
|
|
88
|
+
return {
|
|
89
|
+
allowed: true,
|
|
90
|
+
code: 'external_entitled',
|
|
91
|
+
reason: 'External skill access granted.',
|
|
92
|
+
context
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
allowed: false,
|
|
98
|
+
code: 'external_subscription_required',
|
|
99
|
+
reason: 'External skills require entitlement in server mode. Set BOOTSPRING_SKILLS_ENTITLED=true or use tier=pro/team/enterprise.',
|
|
100
|
+
context
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function filterAccessibleSkills(skillIds, options = {}) {
|
|
105
|
+
const allowed = [];
|
|
106
|
+
const denied = [];
|
|
107
|
+
|
|
108
|
+
for (const skillId of skillIds || []) {
|
|
109
|
+
const decision = checkSkillAccess(skillId, options);
|
|
110
|
+
if (decision.allowed) {
|
|
111
|
+
allowed.push(skillId);
|
|
112
|
+
} else {
|
|
113
|
+
denied.push({
|
|
114
|
+
skillId,
|
|
115
|
+
code: decision.code,
|
|
116
|
+
reason: decision.reason
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return { allowed, denied };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function isPremiumWorkflow(workflow) {
|
|
125
|
+
const tier = String(workflow?.tier || 'free').toLowerCase();
|
|
126
|
+
return tier !== 'free';
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function checkWorkflowAccess(workflow, options = {}) {
|
|
130
|
+
const context = resolveWorkflowAccessContext(options);
|
|
131
|
+
const policy = policies.getPolicyProfile(context.policyProfile, options);
|
|
132
|
+
|
|
133
|
+
if (policies.isWorkflowBlocked(workflow, policy)) {
|
|
134
|
+
return {
|
|
135
|
+
allowed: false,
|
|
136
|
+
code: 'workflow_policy_blocked',
|
|
137
|
+
reason: `Workflow ${workflow?.key || workflow?.name || 'unknown'} is blocked by ${policy.id} policy profile.`,
|
|
138
|
+
context
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (!isPremiumWorkflow(workflow)) {
|
|
143
|
+
return {
|
|
144
|
+
allowed: true,
|
|
145
|
+
code: 'workflow_free',
|
|
146
|
+
reason: 'Workflow is available on free tier.',
|
|
147
|
+
context
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (context.mode !== SERVER_MODE) {
|
|
152
|
+
return {
|
|
153
|
+
allowed: true,
|
|
154
|
+
code: 'workflow_local_mode',
|
|
155
|
+
reason: 'Premium workflows are enabled in local mode.',
|
|
156
|
+
context
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (context.entitled || PRO_TIERS.has(context.tier)) {
|
|
161
|
+
return {
|
|
162
|
+
allowed: true,
|
|
163
|
+
code: 'workflow_entitled',
|
|
164
|
+
reason: 'Premium workflow access granted.',
|
|
165
|
+
context
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
allowed: false,
|
|
171
|
+
code: 'workflow_subscription_required',
|
|
172
|
+
reason: 'Premium workflows require entitlement in server mode. Set BOOTSPRING_WORKFLOWS_ENTITLED=true or use tier=pro/team/enterprise.',
|
|
173
|
+
context
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function filterAccessibleWorkflows(workflows, options = {}) {
|
|
178
|
+
const allowed = [];
|
|
179
|
+
const denied = [];
|
|
180
|
+
|
|
181
|
+
for (const workflow of workflows || []) {
|
|
182
|
+
const decision = checkWorkflowAccess(workflow, options);
|
|
183
|
+
if (decision.allowed) {
|
|
184
|
+
allowed.push(workflow);
|
|
185
|
+
} else {
|
|
186
|
+
denied.push({
|
|
187
|
+
key: workflow?.key,
|
|
188
|
+
name: workflow?.name,
|
|
189
|
+
code: decision.code,
|
|
190
|
+
reason: decision.reason
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return { allowed, denied };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
module.exports = {
|
|
199
|
+
LOCAL_MODE,
|
|
200
|
+
SERVER_MODE,
|
|
201
|
+
resolveAccessContext,
|
|
202
|
+
resolveWorkflowAccessContext,
|
|
203
|
+
isExternalSkill,
|
|
204
|
+
checkSkillAccess,
|
|
205
|
+
filterAccessibleSkills,
|
|
206
|
+
isPremiumWorkflow,
|
|
207
|
+
checkWorkflowAccess,
|
|
208
|
+
filterAccessibleWorkflows
|
|
209
|
+
};
|
package/core/index.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bootspring Core
|
|
3
|
+
* Main exports for the Bootspring package
|
|
4
|
+
*
|
|
5
|
+
* @package bootspring
|
|
6
|
+
* @module core
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const config = require('./config');
|
|
10
|
+
const context = require('./context');
|
|
11
|
+
const utils = require('./utils');
|
|
12
|
+
const policies = require('./policies');
|
|
13
|
+
const entitlements = require('./entitlements');
|
|
14
|
+
const telemetry = require('./telemetry');
|
|
15
|
+
const packageJson = require('../package.json');
|
|
16
|
+
|
|
17
|
+
module.exports = {
|
|
18
|
+
// Version
|
|
19
|
+
VERSION: packageJson.version,
|
|
20
|
+
|
|
21
|
+
// Core modules
|
|
22
|
+
config,
|
|
23
|
+
context,
|
|
24
|
+
utils,
|
|
25
|
+
policies,
|
|
26
|
+
entitlements,
|
|
27
|
+
telemetry,
|
|
28
|
+
|
|
29
|
+
// Convenience exports
|
|
30
|
+
loadConfig: config.load,
|
|
31
|
+
getContext: context.get,
|
|
32
|
+
validateContext: context.validate,
|
|
33
|
+
checkSkillAccess: entitlements.checkSkillAccess,
|
|
34
|
+
checkWorkflowAccess: entitlements.checkWorkflowAccess,
|
|
35
|
+
|
|
36
|
+
// Brand info
|
|
37
|
+
BRAND: {
|
|
38
|
+
name: 'Bootspring',
|
|
39
|
+
tagline: 'Development scaffolding with intelligence',
|
|
40
|
+
website: 'https://bootspring.com',
|
|
41
|
+
docs: 'https://bootspring.com/docs'
|
|
42
|
+
}
|
|
43
|
+
};
|
package/core/policies.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bootspring policy profiles
|
|
3
|
+
* Team-level controls for capability gating.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const DEFAULT_POLICY_PROFILE = 'startup';
|
|
7
|
+
|
|
8
|
+
const POLICY_PROFILES = {
|
|
9
|
+
startup: {
|
|
10
|
+
id: 'startup',
|
|
11
|
+
name: 'Startup',
|
|
12
|
+
allowExternalSkills: true,
|
|
13
|
+
blockedWorkflows: []
|
|
14
|
+
},
|
|
15
|
+
regulated: {
|
|
16
|
+
id: 'regulated',
|
|
17
|
+
name: 'Regulated',
|
|
18
|
+
allowExternalSkills: false,
|
|
19
|
+
blockedWorkflows: ['growth-pack']
|
|
20
|
+
},
|
|
21
|
+
enterprise: {
|
|
22
|
+
id: 'enterprise',
|
|
23
|
+
name: 'Enterprise',
|
|
24
|
+
allowExternalSkills: true,
|
|
25
|
+
blockedWorkflows: []
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
function normalizeProfile(profile) {
|
|
30
|
+
const key = String(profile || DEFAULT_POLICY_PROFILE).trim().toLowerCase();
|
|
31
|
+
return POLICY_PROFILES[key] ? key : DEFAULT_POLICY_PROFILE;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function parseCsvList(value) {
|
|
35
|
+
if (!value) return [];
|
|
36
|
+
return String(value)
|
|
37
|
+
.split(',')
|
|
38
|
+
.map(item => item.trim())
|
|
39
|
+
.filter(Boolean);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function resolvePolicyProfile(options = {}) {
|
|
43
|
+
return normalizeProfile(options.policyProfile || process.env.BOOTSPRING_POLICY_PROFILE);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function getPolicyProfile(profile, options = {}) {
|
|
47
|
+
const key = normalizeProfile(profile);
|
|
48
|
+
const base = POLICY_PROFILES[key];
|
|
49
|
+
const blockedFromEnv = parseCsvList(options.blockedWorkflows || process.env.BOOTSPRING_POLICY_BLOCKED_WORKFLOWS);
|
|
50
|
+
return {
|
|
51
|
+
...base,
|
|
52
|
+
blockedWorkflows: Array.from(new Set([...(base.blockedWorkflows || []), ...blockedFromEnv]))
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function isWorkflowBlocked(workflow, profile) {
|
|
57
|
+
const workflowKey = String(workflow?.key || '').trim();
|
|
58
|
+
if (!workflowKey) return false;
|
|
59
|
+
return (profile.blockedWorkflows || []).includes(workflowKey);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
module.exports = {
|
|
63
|
+
DEFAULT_POLICY_PROFILE,
|
|
64
|
+
POLICY_PROFILES,
|
|
65
|
+
resolvePolicyProfile,
|
|
66
|
+
getPolicyProfile,
|
|
67
|
+
isWorkflowBlocked
|
|
68
|
+
};
|