@girardmedia/bootspring 2.2.0 → 2.3.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.
Files changed (50) hide show
  1. package/README.md +2 -2
  2. package/bin/bootspring.js +35 -96
  3. package/claude-commands/agent.md +34 -0
  4. package/claude-commands/bs.md +31 -0
  5. package/claude-commands/build.md +25 -0
  6. package/claude-commands/skill.md +31 -0
  7. package/claude-commands/todo.md +25 -0
  8. package/dist/cli/index.cjs +17808 -0
  9. package/dist/core/index.d.ts +5814 -0
  10. package/dist/core.js +5780 -0
  11. package/dist/mcp/index.d.ts +1 -0
  12. package/dist/mcp-server.js +2299 -0
  13. package/generators/api-docs.js +2 -2
  14. package/generators/decisions.js +3 -3
  15. package/generators/health.js +16 -16
  16. package/generators/sprint.js +2 -2
  17. package/package.json +27 -59
  18. package/core/api-client.d.ts +0 -69
  19. package/core/api-client.js +0 -1482
  20. package/core/auth.d.ts +0 -98
  21. package/core/auth.js +0 -737
  22. package/core/build-orchestrator.js +0 -508
  23. package/core/build-state.js +0 -612
  24. package/core/config.d.ts +0 -106
  25. package/core/config.js +0 -1328
  26. package/core/context-loader.js +0 -580
  27. package/core/context.d.ts +0 -61
  28. package/core/context.js +0 -327
  29. package/core/entitlements.d.ts +0 -70
  30. package/core/entitlements.js +0 -322
  31. package/core/index.d.ts +0 -53
  32. package/core/index.js +0 -62
  33. package/core/mcp-config.js +0 -115
  34. package/core/policies.d.ts +0 -43
  35. package/core/policies.js +0 -113
  36. package/core/policy-matrix.js +0 -303
  37. package/core/project-activity.js +0 -175
  38. package/core/redaction.d.ts +0 -5
  39. package/core/redaction.js +0 -63
  40. package/core/self-update.js +0 -259
  41. package/core/session.js +0 -353
  42. package/core/task-extractor.js +0 -1098
  43. package/core/telemetry.d.ts +0 -55
  44. package/core/telemetry.js +0 -617
  45. package/core/tier-enforcement.js +0 -928
  46. package/core/utils.d.ts +0 -90
  47. package/core/utils.js +0 -455
  48. package/core/validation.js +0 -572
  49. package/mcp/server.d.ts +0 -57
  50. package/mcp/server.js +0 -264
package/core/context.js DELETED
@@ -1,327 +0,0 @@
1
- /**
2
- * Bootspring Context Manager
3
- * Handles project context for AI assistants
4
- *
5
- * @package bootspring
6
- * @module core/context
7
- */
8
-
9
- const path = require('path');
10
- const config = require('./config');
11
- const utils = require('./utils');
12
-
13
- /**
14
- * Get current project context
15
- * @param {object} [options] - Options
16
- * @returns {object} Context object
17
- */
18
- function get(options = {}) {
19
- const cfg = options.config || config.load();
20
- const projectRoot = cfg._projectRoot;
21
-
22
- const context = {
23
- project: cfg.project,
24
- stack: cfg.stack,
25
- plugins: getEnabledPlugins(cfg),
26
- files: getProjectFiles(projectRoot),
27
- git: getGitInfo(projectRoot),
28
- state: getProjectState(projectRoot, cfg),
29
- timestamp: new Date().toISOString()
30
- };
31
-
32
- return context;
33
- }
34
-
35
- /**
36
- * Get enabled plugins from config
37
- * @param {object} cfg - Configuration
38
- * @returns {object} Enabled plugins
39
- */
40
- function getEnabledPlugins(cfg) {
41
- const enabled = {};
42
-
43
- for (const [name, plugin] of Object.entries(cfg.plugins || {})) {
44
- if (plugin.enabled !== false) {
45
- enabled[name] = {
46
- provider: plugin.provider || 'default',
47
- features: plugin.features || []
48
- };
49
- }
50
- }
51
-
52
- return enabled;
53
- }
54
-
55
- /**
56
- * Get project file structure summary
57
- * @param {string} projectRoot - Project root
58
- * @returns {object} File info
59
- */
60
- function getProjectFiles(projectRoot) {
61
- const files = {
62
- hasPackageJson: utils.fileExists(path.join(projectRoot, 'package.json')),
63
- hasTsConfig: utils.fileExists(path.join(projectRoot, 'tsconfig.json')),
64
- hasClaudeMd: utils.fileExists(path.join(projectRoot, 'CLAUDE.md')),
65
- hasBootspringConfig: utils.fileExists(path.join(projectRoot, 'bootspring.config.js')),
66
- hasTodoMd: utils.fileExists(path.join(projectRoot, 'todo.md')),
67
- hasGit: utils.fileExists(path.join(projectRoot, '.git')),
68
- hasSrcDir: utils.fileExists(path.join(projectRoot, 'src')),
69
- hasAppDir: utils.fileExists(path.join(projectRoot, 'app')),
70
- hasPagesDir: utils.fileExists(path.join(projectRoot, 'pages'))
71
- };
72
-
73
- // Detect framework structure
74
- if (files.hasAppDir) {
75
- files.structure = 'app-router';
76
- } else if (files.hasPagesDir) {
77
- files.structure = 'pages-router';
78
- } else if (files.hasSrcDir) {
79
- files.structure = 'src-based';
80
- } else {
81
- files.structure = 'flat';
82
- }
83
-
84
- return files;
85
- }
86
-
87
- /**
88
- * Get git information
89
- * @param {string} projectRoot - Project root
90
- * @returns {object} Git info
91
- */
92
- function getGitInfo(projectRoot) {
93
- const gitDir = path.join(projectRoot, '.git');
94
-
95
- if (!utils.fileExists(gitDir)) {
96
- return { initialized: false };
97
- }
98
-
99
- const info = { initialized: true };
100
-
101
- // Get current branch
102
- const headPath = path.join(gitDir, 'HEAD');
103
- if (utils.fileExists(headPath)) {
104
- const head = utils.readFile(headPath).trim();
105
- if (head.startsWith('ref: refs/heads/')) {
106
- info.branch = head.replace('ref: refs/heads/', '');
107
- }
108
- }
109
-
110
- // Check for remote
111
- const configPath = path.join(gitDir, 'config');
112
- if (utils.fileExists(configPath)) {
113
- const gitConfig = utils.readFile(configPath);
114
- info.hasRemote = gitConfig.includes('[remote "origin"]');
115
- }
116
-
117
- return info;
118
- }
119
-
120
- /**
121
- * Get project state
122
- * @param {string} projectRoot - Project root
123
- * @param {object} cfg - Configuration
124
- * @returns {object} State info
125
- */
126
- function getProjectState(projectRoot, cfg) {
127
- const state = {
128
- phase: 'unknown',
129
- health: 'unknown',
130
- todos: 0,
131
- lastGenerated: null
132
- };
133
-
134
- // Count todos
135
- const todoPath = path.join(projectRoot, cfg.paths?.todo || 'todo.md');
136
- if (utils.fileExists(todoPath)) {
137
- const content = utils.readFile(todoPath);
138
- const todoMatches = content.match(/- \[ \]/g);
139
- state.todos = todoMatches ? todoMatches.length : 0;
140
- }
141
-
142
- // Check CLAUDE.md generation time
143
- const claudePath = path.join(projectRoot, cfg.paths?.context || 'CLAUDE.md');
144
- if (utils.fileExists(claudePath)) {
145
- state.lastGenerated = utils.getFileTime(claudePath);
146
- }
147
-
148
- // Determine phase
149
- if (!cfg._configPath) {
150
- state.phase = 'uninitialized';
151
- } else if (!state.lastGenerated) {
152
- state.phase = 'initialized';
153
- } else {
154
- state.phase = 'active';
155
- }
156
-
157
- // Determine health
158
- const issues = [];
159
- if (!utils.fileExists(path.join(projectRoot, 'package.json'))) {
160
- issues.push('missing-package-json');
161
- }
162
- if (!cfg._configPath) {
163
- issues.push('missing-config');
164
- }
165
- if (!state.lastGenerated) {
166
- issues.push('missing-context');
167
- }
168
-
169
- if (issues.length === 0) {
170
- state.health = 'good';
171
- } else if (issues.length <= 2) {
172
- state.health = 'fair';
173
- } else {
174
- state.health = 'needs-attention';
175
- }
176
-
177
- state.issues = issues;
178
-
179
- return state;
180
- }
181
-
182
- /**
183
- * Validate project context
184
- * @param {object} [options] - Options
185
- * @returns {object} Validation result
186
- */
187
- function validate(options = {}) {
188
- const cfg = options.config || config.load();
189
- const projectRoot = cfg._projectRoot;
190
-
191
- const checks = [];
192
- let score = 0;
193
- const maxScore = 10;
194
-
195
- // Check 1: Config exists
196
- if (cfg._configPath) {
197
- checks.push({ name: 'Configuration', status: 'pass', message: 'bootspring.config.js found' });
198
- score += 2;
199
- } else {
200
- checks.push({ name: 'Configuration', status: 'fail', message: 'bootspring.config.js missing' });
201
- }
202
-
203
- // Check 2: CLAUDE.md exists
204
- const claudePath = path.join(projectRoot, cfg.paths?.context || 'CLAUDE.md');
205
- if (utils.fileExists(claudePath)) {
206
- checks.push({ name: 'AI Context', status: 'pass', message: 'CLAUDE.md exists' });
207
- score += 2;
208
- } else {
209
- checks.push({ name: 'AI Context', status: 'fail', message: 'CLAUDE.md missing - run bootspring generate' });
210
- }
211
-
212
- // Check 3: package.json exists
213
- if (utils.fileExists(path.join(projectRoot, 'package.json'))) {
214
- checks.push({ name: 'Package', status: 'pass', message: 'package.json found' });
215
- score += 1;
216
- } else {
217
- checks.push({ name: 'Package', status: 'warn', message: 'package.json missing' });
218
- }
219
-
220
- // Check 4: Git initialized
221
- if (utils.fileExists(path.join(projectRoot, '.git'))) {
222
- checks.push({ name: 'Git', status: 'pass', message: 'Git repository initialized' });
223
- score += 1;
224
- } else {
225
- checks.push({ name: 'Git', status: 'warn', message: 'Git not initialized' });
226
- }
227
-
228
- // Check 5: TypeScript config (if using TS)
229
- if (cfg.stack?.language === 'typescript') {
230
- if (utils.fileExists(path.join(projectRoot, 'tsconfig.json'))) {
231
- checks.push({ name: 'TypeScript', status: 'pass', message: 'tsconfig.json found' });
232
- score += 1;
233
- } else {
234
- checks.push({ name: 'TypeScript', status: 'fail', message: 'tsconfig.json missing for TypeScript project' });
235
- }
236
- } else {
237
- score += 1;
238
- }
239
-
240
- // Check 6: Config validation
241
- const configValidation = config.validate(cfg);
242
- if (configValidation.valid) {
243
- checks.push({ name: 'Config Validation', status: 'pass', message: 'Configuration is valid' });
244
- score += 2;
245
- } else {
246
- checks.push({ name: 'Config Validation', status: 'fail', message: configValidation.errors.join(', ') });
247
- }
248
-
249
- // Check 7: Todo file
250
- if (utils.fileExists(path.join(projectRoot, cfg.paths?.todo || 'todo.md'))) {
251
- checks.push({ name: 'Todo Tracking', status: 'pass', message: 'todo.md exists' });
252
- score += 1;
253
- } else {
254
- checks.push({ name: 'Todo Tracking', status: 'fail', message: 'todo.md not found' });
255
- }
256
-
257
- return {
258
- valid: score >= maxScore * 0.6,
259
- score,
260
- maxScore,
261
- percentage: Math.round((score / maxScore) * 100),
262
- checks
263
- };
264
- }
265
-
266
- /**
267
- * Generate context summary for AI
268
- * @param {object} [options] - Options
269
- * @returns {string} Context summary markdown
270
- */
271
- function generateSummary(options = {}) {
272
- const ctx = get(options);
273
-
274
- const lines = [
275
- '# Project Context',
276
- '',
277
- `**Project**: ${ctx.project.name}`,
278
- `**Generated**: ${ctx.timestamp}`,
279
- '',
280
- '## Stack',
281
- `- Framework: ${ctx.stack.framework}`,
282
- `- Language: ${ctx.stack.language}`,
283
- `- Database: ${ctx.stack.database}`,
284
- `- Hosting: ${ctx.stack.hosting}`,
285
- ''
286
- ];
287
-
288
- // Plugins
289
- const enabledPlugins = Object.keys(ctx.plugins);
290
- if (enabledPlugins.length > 0) {
291
- lines.push('## Enabled Plugins');
292
- for (const [name, plugin] of Object.entries(ctx.plugins)) {
293
- lines.push(`- **${name}**: ${plugin.provider}`);
294
- }
295
- lines.push('');
296
- }
297
-
298
- // State
299
- lines.push('## Project State');
300
- lines.push(`- Phase: ${ctx.state.phase}`);
301
- lines.push(`- Health: ${ctx.state.health}`);
302
- lines.push(`- Open Todos: ${ctx.state.todos}`);
303
- if (ctx.state.lastGenerated) {
304
- lines.push(`- Context Last Generated: ${utils.formatRelativeTime(ctx.state.lastGenerated)}`);
305
- }
306
- lines.push('');
307
-
308
- // Git
309
- if (ctx.git.initialized) {
310
- lines.push('## Git');
311
- lines.push(`- Branch: ${ctx.git.branch || 'unknown'}`);
312
- lines.push(`- Remote: ${ctx.git.hasRemote ? 'configured' : 'not configured'}`);
313
- lines.push('');
314
- }
315
-
316
- return lines.join('\n');
317
- }
318
-
319
- module.exports = {
320
- get,
321
- validate,
322
- generateSummary,
323
- getEnabledPlugins,
324
- getProjectFiles,
325
- getGitInfo,
326
- getProjectState
327
- };
@@ -1,70 +0,0 @@
1
- /**
2
- * Bootspring Entitlements Types
3
- * @module core/entitlements
4
- */
5
-
6
- import { Tier } from './auth';
7
-
8
- export interface AccessResult {
9
- allowed: boolean;
10
- reason?: string;
11
- requiredTier?: Tier;
12
- }
13
-
14
- export interface SkillAccess extends AccessResult {
15
- skillId: string;
16
- }
17
-
18
- export interface WorkflowAccess extends AccessResult {
19
- workflowId: string;
20
- }
21
-
22
- /**
23
- * Check if user has access to a skill
24
- * @param skillId - Skill identifier
25
- * @param options - Check options
26
- * @returns Access result
27
- */
28
- export function checkSkillAccess(
29
- skillId: string,
30
- options?: { tier?: Tier; serverMode?: boolean }
31
- ): SkillAccess;
32
-
33
- /**
34
- * Check if user has access to a workflow
35
- * @param workflowId - Workflow identifier
36
- * @param options - Check options
37
- * @returns Access result
38
- */
39
- export function checkWorkflowAccess(
40
- workflowId: string,
41
- options?: { tier?: Tier; serverMode?: boolean }
42
- ): WorkflowAccess;
43
-
44
- /**
45
- * Filter skills by access
46
- * @param skills - List of skill IDs
47
- * @param options - Filter options
48
- * @returns Filtered skills with access info
49
- */
50
- export function filterSkillsByAccess(
51
- skills: string[],
52
- options?: { tier?: Tier }
53
- ): { allowed: string[]; denied: SkillAccess[] };
54
-
55
- /**
56
- * Filter workflows by access
57
- * @param workflows - List of workflow IDs
58
- * @param options - Filter options
59
- * @returns Filtered workflows with access info
60
- */
61
- export function filterWorkflowsByAccess(
62
- workflows: string[],
63
- options?: { tier?: Tier }
64
- ): { allowed: string[]; denied: WorkflowAccess[] };
65
-
66
- /**
67
- * Check if running in server mode
68
- * @returns True if in server mode
69
- */
70
- export function isServerMode(): boolean;
@@ -1,322 +0,0 @@
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 policies = require('./policies');
9
- const KNOWN_TIERS = new Set(['free', 'founder', 'pro', 'team', 'enterprise', 'custom']);
10
-
11
- // Use tier-enforcement for tier checking (lazy-loaded to avoid circular dependency)
12
- let _tierEnforcement = null;
13
- function getTierEnforcement() {
14
- if (!_tierEnforcement) {
15
- _tierEnforcement = require('./tier-enforcement');
16
- }
17
- return _tierEnforcement;
18
- }
19
-
20
- // Check if tier meets pro requirement
21
- function isProTier(tier) {
22
- return getTierEnforcement().meetsTierRequirement('pro', tier);
23
- }
24
-
25
- function parseBoolean(value) {
26
- if (typeof value === 'boolean') return value;
27
- if (value === null || value === undefined) return false;
28
- const normalized = String(value).trim().toLowerCase();
29
- return normalized === '1' || normalized === 'true' || normalized === 'yes' || normalized === 'on';
30
- }
31
-
32
- function normalizeTier(value) {
33
- const tier = String(value || 'free').trim().toLowerCase();
34
- return KNOWN_TIERS.has(tier) ? tier : 'free';
35
- }
36
-
37
- function normalizeMode(value) {
38
- const mode = String(value || '').trim().toLowerCase();
39
- if (mode === SERVER_MODE) {
40
- return SERVER_MODE;
41
- }
42
- return LOCAL_MODE;
43
- }
44
-
45
- function resolveAccessContext(options = {}) {
46
- const envMode = process.env.BOOTSPRING_SKILL_ACCESS_MODE;
47
- const envTier = process.env.BOOTSPRING_USER_TIER;
48
- const envEntitled = process.env.BOOTSPRING_SKILLS_ENTITLED;
49
-
50
- return {
51
- mode: normalizeMode(options.mode || envMode),
52
- tier: normalizeTier(options.tier || envTier || 'free'),
53
- entitled: parseBoolean(options.entitled ?? envEntitled),
54
- policyProfile: policies.resolvePolicyProfile(options)
55
- };
56
- }
57
-
58
- function resolveWorkflowAccessContext(options = {}) {
59
- const envMode = process.env.BOOTSPRING_WORKFLOW_ACCESS_MODE || process.env.BOOTSPRING_SKILL_ACCESS_MODE;
60
- const envTier = process.env.BOOTSPRING_USER_TIER;
61
- const envEntitled = process.env.BOOTSPRING_WORKFLOWS_ENTITLED ?? process.env.BOOTSPRING_SKILLS_ENTITLED;
62
-
63
- return {
64
- mode: normalizeMode(options.mode || envMode),
65
- tier: normalizeTier(options.tier || envTier || 'free'),
66
- entitled: parseBoolean(options.entitled ?? envEntitled),
67
- policyProfile: policies.resolvePolicyProfile(options)
68
- };
69
- }
70
-
71
- function isExternalSkill(skillId) {
72
- return String(skillId || '').trim().toLowerCase().startsWith('external/');
73
- }
74
-
75
- function isPremiumPattern(skillTier) {
76
- const tier = String(skillTier || 'free').trim().toLowerCase();
77
- return tier === 'pro' || tier === 'premium';
78
- }
79
-
80
- function checkSkillAccess(skillId, options = {}) {
81
- const context = resolveAccessContext(options);
82
- const skillTier = options.skillTier;
83
-
84
- // External skills
85
- if (isExternalSkill(skillId)) {
86
- const policy = policies.getPolicyProfile(context.policyProfile, options);
87
- if (!policy.allowExternalSkills) {
88
- return {
89
- allowed: false,
90
- code: 'external_policy_blocked',
91
- reason: `External skills are blocked by ${policy.id} policy profile.`,
92
- context
93
- };
94
- }
95
-
96
- if (context.mode !== SERVER_MODE) {
97
- return {
98
- allowed: true,
99
- code: 'external_local_mode',
100
- reason: 'External skills are enabled in local mode.',
101
- context
102
- };
103
- }
104
-
105
- if (context.entitled || isProTier(context.tier)) {
106
- return {
107
- allowed: true,
108
- code: 'external_entitled',
109
- reason: 'External skill access granted.',
110
- context
111
- };
112
- }
113
-
114
- return {
115
- allowed: false,
116
- code: 'external_subscription_required',
117
- reason: 'External skills require entitlement in server mode. Set BOOTSPRING_SKILLS_ENTITLED=true or use tier=pro/team/enterprise.',
118
- context
119
- };
120
- }
121
-
122
- // Local mode - all patterns accessible for development
123
- if (context.mode !== SERVER_MODE) {
124
- if (isPremiumPattern(skillTier)) {
125
- return {
126
- allowed: true,
127
- code: 'premium_local_mode',
128
- reason: 'Premium patterns are enabled in local mode.',
129
- context
130
- };
131
- }
132
- return {
133
- allowed: true,
134
- code: 'free_local_mode',
135
- reason: 'Patterns are enabled in local mode.',
136
- context
137
- };
138
- }
139
-
140
- // Server mode - all patterns require authentication
141
- // Check if user has any entitlement (authenticated)
142
- const isAuthenticated = context.entitled || context.tier !== 'free' || isProTier(context.tier);
143
-
144
- if (!isAuthenticated) {
145
- // Unauthenticated in server mode - block all patterns
146
- return {
147
- allowed: false,
148
- code: 'authentication_required',
149
- reason: 'Patterns require authentication in server mode. Sign in at bootspring.com or use BOOTSPRING_SKILLS_ENTITLED=true for development.',
150
- context
151
- };
152
- }
153
-
154
- // Authenticated - check tier for premium patterns
155
- if (isPremiumPattern(skillTier)) {
156
- if (isProTier(context.tier)) {
157
- return {
158
- allowed: true,
159
- code: 'premium_entitled',
160
- reason: 'Premium pattern access granted.',
161
- context
162
- };
163
- }
164
- return {
165
- allowed: false,
166
- code: 'premium_subscription_required',
167
- reason: 'Premium pattern requires Pro subscription. Upgrade at bootspring.com/pricing.',
168
- context
169
- };
170
- }
171
-
172
- // Authenticated free tier user accessing free pattern
173
- return {
174
- allowed: true,
175
- code: 'free_entitled',
176
- reason: 'Free tier pattern access granted.',
177
- context
178
- };
179
- }
180
-
181
- function filterAccessibleSkills(skillIds, options = {}) {
182
- const allowed = [];
183
- const denied = [];
184
-
185
- for (const skillId of skillIds || []) {
186
- const decision = checkSkillAccess(skillId, options);
187
- if (decision.allowed) {
188
- allowed.push(skillId);
189
- } else {
190
- denied.push({
191
- skillId,
192
- code: decision.code,
193
- reason: decision.reason
194
- });
195
- }
196
- }
197
-
198
- return { allowed, denied };
199
- }
200
-
201
- function isPremiumWorkflow(workflow) {
202
- const tier = String(workflow?.tier || 'free').trim().toLowerCase();
203
- return tier !== 'free';
204
- }
205
-
206
- // Lazy-load pack lifecycle to avoid circular dependency
207
- let _packLifecycle = null;
208
- function getPackLifecycle() {
209
- if (!_packLifecycle) {
210
- try {
211
- _packLifecycle = require('../intelligence/orchestrator/config/pack-lifecycle');
212
- } catch {
213
- _packLifecycle = { isPackVisibleToUser: () => true, getPackStage: () => 'ga' };
214
- }
215
- }
216
- return _packLifecycle;
217
- }
218
-
219
- function checkWorkflowAccess(workflow, options = {}) {
220
- const context = resolveWorkflowAccessContext(options);
221
- const policy = policies.getPolicyProfile(context.policyProfile, options);
222
-
223
- // Check if workflow is blocked by policy
224
- if (policies.isWorkflowBlocked(workflow, policy)) {
225
- return {
226
- allowed: false,
227
- code: 'workflow_policy_blocked',
228
- reason: `Workflow ${workflow?.key || workflow?.name || 'unknown'} is blocked by ${policy.id} policy profile.`,
229
- context
230
- };
231
- }
232
-
233
- // Check pack lifecycle visibility (for premium packs)
234
- const packName = workflow?.pack;
235
- if (packName) {
236
- const lifecycle = getPackLifecycle();
237
- const isVisible = lifecycle.isPackVisibleToUser(packName, {
238
- tier: context.tier,
239
- userId: options.userId,
240
- deviceId: options.deviceId
241
- });
242
-
243
- if (!isVisible) {
244
- const stage = lifecycle.getPackStage(packName);
245
- return {
246
- allowed: false,
247
- code: 'pack_not_available',
248
- reason: `Pack "${packName}" is not available in your rollout group (stage: ${stage})`,
249
- context,
250
- stage
251
- };
252
- }
253
- }
254
-
255
- if (!isPremiumWorkflow(workflow)) {
256
- return {
257
- allowed: true,
258
- code: 'workflow_free',
259
- reason: 'Workflow is available on free tier.',
260
- context
261
- };
262
- }
263
-
264
- if (context.mode !== SERVER_MODE) {
265
- return {
266
- allowed: true,
267
- code: 'workflow_local_mode',
268
- reason: 'Premium workflows are enabled in local mode.',
269
- context
270
- };
271
- }
272
-
273
- if (context.entitled || isProTier(context.tier)) {
274
- return {
275
- allowed: true,
276
- code: 'workflow_entitled',
277
- reason: 'Premium workflow access granted.',
278
- context
279
- };
280
- }
281
-
282
- return {
283
- allowed: false,
284
- code: 'workflow_subscription_required',
285
- reason: 'Premium workflows require entitlement in server mode. Set BOOTSPRING_WORKFLOWS_ENTITLED=true or use tier=pro/team/enterprise.',
286
- context
287
- };
288
- }
289
-
290
- function filterAccessibleWorkflows(workflows, options = {}) {
291
- const allowed = [];
292
- const denied = [];
293
-
294
- for (const workflow of workflows || []) {
295
- const decision = checkWorkflowAccess(workflow, options);
296
- if (decision.allowed) {
297
- allowed.push(workflow);
298
- } else {
299
- denied.push({
300
- key: workflow?.key,
301
- name: workflow?.name,
302
- code: decision.code,
303
- reason: decision.reason
304
- });
305
- }
306
- }
307
-
308
- return { allowed, denied };
309
- }
310
-
311
- module.exports = {
312
- LOCAL_MODE,
313
- SERVER_MODE,
314
- resolveAccessContext,
315
- resolveWorkflowAccessContext,
316
- isExternalSkill,
317
- checkSkillAccess,
318
- filterAccessibleSkills,
319
- isPremiumWorkflow,
320
- checkWorkflowAccess,
321
- filterAccessibleWorkflows
322
- };